diff options
Diffstat (limited to 'src')
225 files changed, 41863 insertions, 0 deletions
diff --git a/src/AtomReader.cpp b/src/AtomReader.cpp new file mode 100644 index 00000000..218110e4 --- /dev/null +++ b/src/AtomReader.cpp @@ -0,0 +1,384 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <utility> + +#include "ingen/AtomReader.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Message.hpp" +#include "ingen/Node.hpp" +#include "ingen/URIMap.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "raul/Path.hpp" + +namespace Ingen { + +AtomReader::AtomReader(URIMap& map, URIs& uris, Log& log, Interface& iface) + : _map(map) + , _uris(uris) + , _log(log) + , _iface(iface) +{} + +void +AtomReader::get_atom(const LV2_Atom* in, Atom& out) +{ + if (in) { + if (in->type == _uris.atom_URID) { + const LV2_Atom_URID* urid = (const LV2_Atom_URID*)in; + const char* uri = _map.unmap_uri(urid->body); + if (uri) { + out = Atom(sizeof(int32_t), _uris.atom_URID, &urid->body); + } else { + _log.error(fmt("Unable to unmap URID %1%\n") % urid->body); + } + } else { + out = Atom(in->size, in->type, LV2_ATOM_BODY_CONST(in)); + } + } +} + +void +AtomReader::get_props(const LV2_Atom_Object* obj, + Ingen::Properties& props) +{ + if (obj->body.otype) { + const Atom type(sizeof(int32_t), _uris.atom_URID, &obj->body.otype); + props.emplace(_uris.rdf_type, type); + } + LV2_ATOM_OBJECT_FOREACH(obj, p) { + Atom val; + get_atom(&p->value, val); + props.emplace(URI(_map.unmap_uri(p->key)), val); + } +} + +boost::optional<URI> +AtomReader::atom_to_uri(const LV2_Atom* atom) +{ + if (!atom) { + return boost::optional<URI>(); + } else if (atom->type == _uris.atom_URI) { + const char* str = (const char*)LV2_ATOM_BODY_CONST(atom); + if (URI::is_valid(str)) { + return URI(str); + } else { + _log.warn(fmt("Invalid URI <%1%>\n") % str); + } + } else if (atom->type == _uris.atom_Path) { + const char* str = (const char*)LV2_ATOM_BODY_CONST(atom); + if (!strncmp(str, "file://", 5)) { + return URI(str); + } else { + return URI(std::string("file://") + str); + } + } else if (atom->type == _uris.atom_URID) { + const char* str = _map.unmap_uri(((const LV2_Atom_URID*)atom)->body); + if (str) { + return URI(str); + } else { + _log.warn(fmt("Unknown URID %1%\n") % str); + } + } + return boost::optional<URI>(); +} + +boost::optional<Raul::Path> +AtomReader::atom_to_path(const LV2_Atom* atom) +{ + boost::optional<URI> uri = atom_to_uri(atom); + if (uri && uri_is_path(*uri)) { + return uri_to_path(*uri); + } + return boost::optional<Raul::Path>(); +} + +Resource::Graph +AtomReader::atom_to_context(const LV2_Atom* atom) +{ + Resource::Graph ctx = Resource::Graph::DEFAULT; + if (atom) { + boost::optional<URI> maybe_uri = atom_to_uri(atom); + if (maybe_uri) { + ctx = Resource::uri_to_graph(*maybe_uri); + } else { + _log.warn("Message has invalid context\n"); + } + } + return ctx; +} + +bool +AtomReader::is_message(const URIs& uris, const LV2_Atom* msg) +{ + if (msg->type != uris.atom_Object) { + return false; + } + + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)msg; + return (obj->body.otype == uris.patch_Get || + obj->body.otype == uris.patch_Delete || + obj->body.otype == uris.patch_Put || + obj->body.otype == uris.patch_Set || + obj->body.otype == uris.patch_Patch || + obj->body.otype == uris.patch_Move || + obj->body.otype == uris.patch_Response); +} + +bool +AtomReader::write(const LV2_Atom* msg, int32_t default_id) +{ + if (msg->type != _uris.atom_Object) { + _log.warn(fmt("Unknown message type <%1%>\n") + % _map.unmap_uri(msg->type)); + return false; + } + + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)msg; + const LV2_Atom* subject = nullptr; + const LV2_Atom* number = nullptr; + + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_subject, &subject, + (LV2_URID)_uris.patch_sequenceNumber, &number, + NULL); + + const boost::optional<URI> subject_uri = atom_to_uri(subject); + + const int32_t seq = ((number && number->type == _uris.atom_Int) + ? ((const LV2_Atom_Int*)number)->body + : default_id); + + if (obj->body.otype == _uris.patch_Get) { + if (subject_uri) { + _iface(Get{seq, *subject_uri}); + } + } else if (obj->body.otype == _uris.ingen_BundleStart) { + _iface(BundleBegin{seq}); + } else if (obj->body.otype == _uris.ingen_BundleEnd) { + _iface(BundleEnd{seq}); + } else if (obj->body.otype == _uris.patch_Delete) { + const LV2_Atom_Object* body = nullptr; + lv2_atom_object_get(obj, (LV2_URID)_uris.patch_body, &body, 0); + + if (subject_uri && !body) { + _iface(Del{seq, *subject_uri}); + return true; + } else if (body && body->body.otype == _uris.ingen_Arc) { + const LV2_Atom* tail = nullptr; + const LV2_Atom* head = nullptr; + const LV2_Atom* incidentTo = nullptr; + lv2_atom_object_get(body, + (LV2_URID)_uris.ingen_tail, &tail, + (LV2_URID)_uris.ingen_head, &head, + (LV2_URID)_uris.ingen_incidentTo, &incidentTo, + NULL); + + boost::optional<Raul::Path> subject_path(atom_to_path(subject)); + boost::optional<Raul::Path> tail_path(atom_to_path(tail)); + boost::optional<Raul::Path> head_path(atom_to_path(head)); + boost::optional<Raul::Path> other_path(atom_to_path(incidentTo)); + if (tail_path && head_path) { + _iface(Disconnect{seq, *tail_path, *head_path}); + } else if (subject_path && other_path) { + _iface(DisconnectAll{seq, *subject_path, *other_path}); + } else { + _log.warn("Delete of unknown object\n"); + return false; + } + } + } else if (obj->body.otype == _uris.patch_Put) { + const LV2_Atom_Object* body = nullptr; + const LV2_Atom* context = nullptr; + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_body, &body, + (LV2_URID)_uris.patch_context, &context, + 0); + if (!body) { + _log.warn("Put message has no body\n"); + return false; + } else if (!subject_uri) { + _log.warn("Put message has no subject\n"); + return false; + } + + if (body->body.otype == _uris.ingen_Arc) { + LV2_Atom* tail = nullptr; + LV2_Atom* head = nullptr; + lv2_atom_object_get(body, + (LV2_URID)_uris.ingen_tail, &tail, + (LV2_URID)_uris.ingen_head, &head, + NULL); + if (!tail || !head) { + _log.warn("Arc has no tail or head\n"); + return false; + } + + boost::optional<Raul::Path> tail_path(atom_to_path(tail)); + boost::optional<Raul::Path> head_path(atom_to_path(head)); + if (tail_path && head_path) { + _iface(Connect{seq, *tail_path, *head_path}); + } else { + _log.warn("Arc has non-path tail or head\n"); + } + } else { + Ingen::Properties props; + get_props(body, props); + _iface(Put{seq, *subject_uri, props, atom_to_context(context)}); + } + } else if (obj->body.otype == _uris.patch_Set) { + if (!subject_uri) { + _log.warn("Set message has no subject\n"); + return false; + } + + const LV2_Atom_URID* prop = nullptr; + const LV2_Atom* value = nullptr; + const LV2_Atom* context = nullptr; + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_property, &prop, + (LV2_URID)_uris.patch_value, &value, + (LV2_URID)_uris.patch_context, &context, + 0); + if (!prop || ((const LV2_Atom*)prop)->type != _uris.atom_URID) { + _log.warn("Set message missing property\n"); + return false; + } else if (!value) { + _log.warn("Set message missing value\n"); + return false; + } + + Atom atom; + get_atom(value, atom); + _iface(SetProperty{seq, + *subject_uri, + URI(_map.unmap_uri(prop->body)), + atom, + atom_to_context(context)}); + } else if (obj->body.otype == _uris.patch_Patch) { + if (!subject_uri) { + _log.warn("Patch message has no subject\n"); + return false; + } + + const LV2_Atom_Object* remove = nullptr; + const LV2_Atom_Object* add = nullptr; + const LV2_Atom* context = nullptr; + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_remove, &remove, + (LV2_URID)_uris.patch_add, &add, + (LV2_URID)_uris.patch_context, &context, + 0); + if (!remove) { + _log.warn("Patch message has no remove\n"); + return false; + } else if (!add) { + _log.warn("Patch message has no add\n"); + return false; + } + + Ingen::Properties add_props; + get_props(add, add_props); + + Ingen::Properties remove_props; + get_props(remove, remove_props); + + _iface(Delta{seq, *subject_uri, remove_props, add_props, + atom_to_context(context)}); + } else if (obj->body.otype == _uris.patch_Copy) { + if (!subject) { + _log.warn("Copy message has no subject\n"); + return false; + } + + const LV2_Atom* dest = nullptr; + lv2_atom_object_get(obj, (LV2_URID)_uris.patch_destination, &dest, 0); + if (!dest) { + _log.warn("Copy message has no destination\n"); + return false; + } + + boost::optional<URI> subject_uri(atom_to_uri(subject)); + if (!subject_uri) { + _log.warn("Copy message has non-path subject\n"); + return false; + } + + boost::optional<URI> dest_uri(atom_to_uri(dest)); + if (!dest_uri) { + _log.warn("Copy message has non-URI destination\n"); + return false; + } + + _iface(Copy{seq, *subject_uri, *dest_uri}); + } else if (obj->body.otype == _uris.patch_Move) { + if (!subject) { + _log.warn("Move message has no subject\n"); + return false; + } + + const LV2_Atom* dest = nullptr; + lv2_atom_object_get(obj, (LV2_URID)_uris.patch_destination, &dest, 0); + if (!dest) { + _log.warn("Move message has no destination\n"); + return false; + } + + boost::optional<Raul::Path> subject_path(atom_to_path(subject)); + if (!subject_path) { + _log.warn("Move message has non-path subject\n"); + return false; + } + + boost::optional<Raul::Path> dest_path(atom_to_path(dest)); + if (!dest_path) { + _log.warn("Move message has non-path destination\n"); + return false; + } + + _iface(Move{seq, *subject_path, *dest_path}); + } else if (obj->body.otype == _uris.patch_Response) { + const LV2_Atom* seq = nullptr; + const LV2_Atom* body = nullptr; + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_sequenceNumber, &seq, + (LV2_URID)_uris.patch_body, &body, + 0); + if (!seq || seq->type != _uris.atom_Int) { + _log.warn("Response message has no sequence number\n"); + return false; + } else if (!body || body->type != _uris.atom_Int) { + _log.warn("Response message body is not integer\n"); + return false; + } + _iface(Response{((const LV2_Atom_Int*)seq)->body, + (Ingen::Status)((const LV2_Atom_Int*)body)->body, + subject_uri ? subject_uri->c_str() : ""}); + } else if (obj->body.otype == _uris.ingen_BundleStart) { + _iface(BundleBegin{seq}); + } else if (obj->body.otype == _uris.ingen_BundleEnd) { + _iface(BundleEnd{seq}); + } else { + _log.warn(fmt("Unknown object type <%1%>\n") + % _map.unmap_uri(obj->body.otype)); + } + + return true; +} + +} // namespace Ingen diff --git a/src/AtomWriter.cpp b/src/AtomWriter.cpp new file mode 100644 index 00000000..f235aafc --- /dev/null +++ b/src/AtomWriter.cpp @@ -0,0 +1,652 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +/** @page protocol Ingen Protocol + * @tableofcontents + * + * @section introduction Controlling Ingen + * + * Ingen is controlled via a simple RDF-based protocol. This conceptual + * protocol can be used in two concrete ways: + * + * 1. When Ingen is running as a process, a socket accepts messages in + * (textual) Turtle syntax, and responds in the same syntax. Transfers are + * delimited by NULL characters in the stream, so the client knows when to + * finish parsing to interpret responses. By default, Ingen listens on + * unix:///tmp/ingen.sock and tcp://localhost:16180 + * + * 2. When Ingen is running as an LV2 plugin, the control and notify ports + * accept and respond using (binary) LV2 atoms. Messages are read and written + * as events with atom:Object bodies. The standard rules for LV2 event + * transmission apply, but note that the graph manipulations described here are + * executed asynchronously and not with sample accuracy, so the response may + * come at a later frame or cycle. + * + * For documentation purposes, this page discusses messages in Turtle syntax, + * but the same protocol is used in the LV2 case, just in a more compact binary + * encoding. + * + * Conceptually, Ingen represents a tree of objects, each of which has a path + * (like /main/in or /main/osc/out) and a set of properties. A property is a + * URI key and some value. All changes to Ingen are represented as + * manipulations of this tree of dictionaries. The core of the protocol is + * based on the LV2 patch extension, which defines several messages for + * manipulating data in this model which resemble HTTP methods. + */ + +#include <cassert> +#include <cstdlib> +#include <string> + +#include <boost/variant/apply_visitor.hpp> + +#include "ingen/AtomSink.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/Node.hpp" +#include "ingen/URIMap.hpp" +#include "raul/Path.hpp" +#include "serd/serd.h" + +namespace Ingen { + +AtomWriter::AtomWriter(URIMap& map, URIs& uris, AtomSink& sink) + : _map(map) + , _uris(uris) + , _sink(sink) +{ + lv2_atom_forge_init(&_forge, &map.urid_map_feature()->urid_map); + _out.set_forge_sink(&_forge); +} + +void +AtomWriter::finish_msg() +{ + assert(!_forge.stack); + _sink.write(_out.atom()); + _out.clear(); +} + +void +AtomWriter::message(const Message& message) +{ + boost::apply_visitor(*this, message); +} + +/** @page protocol + * @subsection Bundles + * + * An [ingen:BundleStart](http://drobilla.net/ns/ingen#BundleStart) marks the + * start of a bundle in the operation stream. A bundle groups a series of + * messages for coarse-grained undo or atomic execution. + * + * @code{.ttl} + * [] a ingen:BundleStart . + * @endcode + + * An [ingen:BundleEnd](http://drobilla.net/ns/ingen#BundleEnd) marks the end + * of a bundle in the operation stream. + * + * @code{.ttl} + * [] a ingen:BundleEnd . + * @endcode + */ +void +AtomWriter::operator()(const BundleBegin& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.ingen_BundleStart, message.seq); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +void +AtomWriter::operator()(const BundleEnd& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.ingen_BundleEnd, message.seq); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +void +AtomWriter::forge_uri(const URI& uri) +{ + if (serd_uri_string_has_scheme((const uint8_t*)uri.c_str())) { + lv2_atom_forge_urid(&_forge, _map.map_uri(uri.c_str())); + } else { + lv2_atom_forge_uri(&_forge, uri.c_str(), uri.length()); + } +} + +void +AtomWriter::forge_properties(const Properties& properties) +{ + for (auto p : properties) { + lv2_atom_forge_key(&_forge, _map.map_uri(p.first.c_str())); + if (p.second.type() == _forge.URI) { + forge_uri(URI(p.second.ptr<char>())); + } else { + lv2_atom_forge_atom(&_forge, p.second.size(), p.second.type()); + lv2_atom_forge_write(&_forge, p.second.get_body(), p.second.size()); + } + } +} + +void +AtomWriter::forge_arc(const Raul::Path& tail, const Raul::Path& head) +{ + LV2_Atom_Forge_Frame arc; + lv2_atom_forge_object(&_forge, &arc, 0, _uris.ingen_Arc); + lv2_atom_forge_key(&_forge, _uris.ingen_tail); + forge_uri(path_to_uri(tail)); + lv2_atom_forge_key(&_forge, _uris.ingen_head); + forge_uri(path_to_uri(head)); + lv2_atom_forge_pop(&_forge, &arc); +} + +void +AtomWriter::forge_request(LV2_Atom_Forge_Frame* frame, LV2_URID type, int32_t id) +{ + lv2_atom_forge_object(&_forge, frame, 0, type); + + if (id) { + lv2_atom_forge_key(&_forge, _uris.patch_sequenceNumber); + lv2_atom_forge_int(&_forge, id); + } +} + +void +AtomWriter::forge_context(Resource::Graph ctx) +{ + switch (ctx) { + case Resource::Graph::EXTERNAL: + lv2_atom_forge_key(&_forge, _uris.patch_context); + forge_uri(_uris.ingen_externalContext); + break; + case Resource::Graph::INTERNAL: + lv2_atom_forge_key(&_forge, _uris.patch_context); + forge_uri(_uris.ingen_internalContext); + break; + default: break; + } +} + +/** @page protocol + * @section methods Methods + * @subsection Put + * + * Send a [Put](http://lv2plug.in/ns/ext/patch#Put) to set properties on an + * object, creating it if necessary. + * + * If the object already exists, all existing object properties with keys that + * match any property in the message are first removed. Other properties are + * left unchanged. + * + * If the object does not yet exist, the message must contain sufficient + * information to create it, including at least one rdf:type property. + * + * @code{.ttl} + * [] + * a patch:Put ; + * patch:subject </main/osc> ; + * patch:body [ + * a ingen:Block ; + * lv2:prototype <http: //drobilla.net/plugins/mda/Shepard> + * ] . + * @endcode + */ +void +AtomWriter::operator()(const Put& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Put, message.seq); + forge_context(message.ctx); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.uri); + lv2_atom_forge_key(&_forge, _uris.patch_body); + + LV2_Atom_Forge_Frame body; + lv2_atom_forge_object(&_forge, &body, 0, 0); + forge_properties(message.properties); + lv2_atom_forge_pop(&_forge, &body); + + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Patch + * + * Send a [Patch](http://lv2plug.in/ns/ext/patch#Patch) to manipulate the + * properties of an object. A set of properties are first removed, then + * another is added. Analogous to WebDAV PROPPATCH. + * + * The special value [patch:wildcard](http://lv2plug.in/ns/ext/patch#wildcard) + * may be used to specify that any value with the given key should be removed. + * + * @code{.ttl} + * [] + * a patch:Patch ; + * patch:subject </main/osc> ; + * patch:add [ + * lv2:name "Osckillator" ; + * ingen:canvasX 32.0 ; + * ingen:canvasY 32.0 ; + * ] ; + * patch:remove [ + * eg:name "Old name" ; # Remove specific value + * ingen:canvasX patch:wildcard ; # Remove all + * ingen:canvasY patch:wildcard ; # Remove all + * ] . + * @endcode + */ +void +AtomWriter::operator()(const Delta& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Patch, message.seq); + forge_context(message.ctx); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.uri); + + lv2_atom_forge_key(&_forge, _uris.patch_remove); + LV2_Atom_Forge_Frame remove_obj; + lv2_atom_forge_object(&_forge, &remove_obj, 0, 0); + forge_properties(message.remove); + lv2_atom_forge_pop(&_forge, &remove_obj); + + lv2_atom_forge_key(&_forge, _uris.patch_add); + LV2_Atom_Forge_Frame add_obj; + lv2_atom_forge_object(&_forge, &add_obj, 0, 0); + forge_properties(message.add); + lv2_atom_forge_pop(&_forge, &add_obj); + + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Copy + * + * Send a [Copy](http://lv2plug.in/ns/ext/copy#Copy) to copy an object from + * its current location (subject) to another (destination). + * + * If both the subject and destination are inside Ingen, like block paths, then the old object + * is copied by, for example, creating a new plugin instance. + * + * If the subject is a filename (file URI or atom:Path) and the destination is + * inside Ingen, then the subject must be an Ingen graph file or bundle, which + * is loaded to the specified destination path. + * + * If the subject is inside Ingen and the destination is a filename, then the + * subject is saved to an Ingen bundle at the given destination. + * + * @code{.ttl} + * [] + * a patch:Copy ; + * patch:subject </main/osc> ; + * patch:destination </main/osc2> . + * @endcode + */ +void +AtomWriter::operator()(const Copy& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Copy, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.old_uri); + lv2_atom_forge_key(&_forge, _uris.patch_destination); + forge_uri(message.new_uri); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Move + * + * Send a [Move](http://lv2plug.in/ns/ext/move#Move) to move an object from its + * current location (subject) to another (destination). + * + * Both subject and destination must be paths in Ingen with the same parent, + * moving between graphs is currently not supported. + * + * @code{.ttl} + * [] + * a patch:Move ; + * patch:subject </main/osc> ; + * patch:destination </main/osc2> . + * @endcode + */ +void +AtomWriter::operator()(const Move& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Move, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(path_to_uri(message.old_path)); + lv2_atom_forge_key(&_forge, _uris.patch_destination); + forge_uri(path_to_uri(message.new_path)); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Delete + * + * Send a [Delete](http://lv2plug.in/ns/ext/delete#Delete) to remove an + * object from the engine and destroy it. + * + * All properties of the object are lost, as are all references to the object + * (e.g. any connections to it). + * + * @code{.ttl} + * [] + * a patch:Delete ; + * patch:subject </main/osc> . + * @endcode + */ +void +AtomWriter::operator()(const Del& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Delete, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.uri); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Set + * + * Send a [Set](http://lv2plug.in/ns/ext/patch#Set) to set a property on an + * object. Any existing value for that property is removed. + * + * @code{.ttl} + * [] + * a patch:Set ; + * patch:subject </main/osc> ; + * patch:property lv2:name ; + * patch:value "Oscwellator" . + * @endcode + */ +void +AtomWriter::operator()(const SetProperty& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Set, message.seq); + forge_context(message.ctx); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.subject); + lv2_atom_forge_key(&_forge, _uris.patch_property); + lv2_atom_forge_urid(&_forge, _map.map_uri(message.predicate.c_str())); + lv2_atom_forge_key(&_forge, _uris.patch_value); + lv2_atom_forge_atom(&_forge, message.value.size(), message.value.type()); + lv2_atom_forge_write(&_forge, message.value.get_body(), message.value.size()); + + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Undo + * + * Use [ingen:Undo](http://drobilla.net/ns/ingen#Undo) to undo the last change + * to the engine. Undo transactions can be delimited using bundle markers, if + * the last operations(s) received were in a bundle, then an Undo will undo the + * effects of that entire bundle. + * + * @code{.ttl} + * [] a ingen:Undo . + * @endcode + */ +void +AtomWriter::operator()(const Undo& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.ingen_Undo, message.seq); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Undo + * + * Use [ingen:Redo](http://drobilla.net/ns/ingen#Redo) to redo the last undone change. + * + * @code{.ttl} + * [] a ingen:Redo . + * @endcode + */ +void +AtomWriter::operator()(const Redo& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.ingen_Redo, message.seq); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Get + * + * Send a [Get](http://lv2plug.in/ns/ext/patch#Get) to get the description + * of the subject. + * + * @code{.ttl} + * [] + * a patch:Get ; + * patch:subject </main/osc> . + * @endcode + */ +void +AtomWriter::operator()(const Get& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Get, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.subject); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * + * @section arcs Arc Manipulation + */ + +/** @page protocol + * @subsection Connect Connecting Ports + * + * Ports are connected by putting an arc with the desired tail (an output port) + * and head (an input port). The tail and head must both be within the + * subject, which must be a graph. + * + * @code{.ttl} + * [] + * a patch:Put ; + * patch:subject </main/> ; + * patch:body [ + * a ingen:Arc ; + * ingen:tail </main/osc/out> ; + * ingen:head </main/filt/in> ; + * ] . + * @endcode + */ +void +AtomWriter::operator()(const Connect& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Put, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(path_to_uri(Raul::Path::lca(message.tail, message.head))); + lv2_atom_forge_key(&_forge, _uris.patch_body); + forge_arc(message.tail, message.head); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Disconnect Disconnecting Ports + * + * Ports are disconnected by deleting the arc between them. The description of + * the arc is the same as in the put command used to create the connection. + * + * @code{.ttl} + * [] + * a patch:Delete ; + * patch:body [ + * a ingen:Arc ; + * ingen:tail </main/osc/out> ; + * ingen:head </main/filt/in> ; + * ] . + * @endcode + */ +void +AtomWriter::operator()(const Disconnect& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Delete, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_body); + forge_arc(message.tail, message.head); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection DisconnectAll Fully Disconnecting Anything + * + * All arcs to or from anything can be removed by giving the special property + * ingen:incidentTo rather than a specific head and tail. This works for both + * ports and blocks (where the effect is to disconnect everything from ports on + * that block). + * + * @code{.ttl} + * [] + * a patch:Delete ; + * patch:subject </main> ; + * patch:body [ + * a ingen:Arc ; + * ingen:incidentTo </main/osc/out> + * ] . + * @endcode + */ +void +AtomWriter::operator()(const DisconnectAll& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Delete, message.seq); + + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(path_to_uri(message.graph)); + + lv2_atom_forge_key(&_forge, _uris.patch_body); + LV2_Atom_Forge_Frame arc; + lv2_atom_forge_object(&_forge, &arc, 0, _uris.ingen_Arc); + lv2_atom_forge_key(&_forge, _uris.ingen_incidentTo); + forge_uri(path_to_uri(message.path)); + lv2_atom_forge_pop(&_forge, &arc); + + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @section Responses + * + * Ingen responds to requests if the patch:sequenceNumber property is set. For + * example: + * @code{.ttl} + * [] + * a patch:Get ; + * patch:sequenceNumber 42 ; + * patch:subject </main/osc> . + * @endcode + * + * Might receive a response like: + * @code{.ttl} + * [] + * a patch:Response ; + * patch:sequenceNumber 42 ; + * patch:subject </main/osc> ; + * patch:body 0 . + * @endcode + * + * Where 0 is a status code, 0 meaning success and any other value being an + * error. Information about status codes, including error message strings, + * are defined in ingen.lv2/errors.ttl. + * + * Note that a response is only a status response, operations that manipulate + * the graph may generate new data on the stream, e.g. the above get request + * would also receive a put that describes /main/osc in the stream immediately + * following the response. + */ +void +AtomWriter::operator()(const Response& response) +{ + const auto& subject = response.subject; + if (!response.id) { + return; + } + + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Response, 0); + lv2_atom_forge_key(&_forge, _uris.patch_sequenceNumber); + lv2_atom_forge_int(&_forge, response.id); + if (!subject.empty()) { + lv2_atom_forge_key(&_forge, _uris.patch_subject); + lv2_atom_forge_uri(&_forge, subject.c_str(), subject.length()); + } + lv2_atom_forge_key(&_forge, _uris.patch_body); + lv2_atom_forge_int(&_forge, static_cast<int>(response.status)); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +void +AtomWriter::operator()(const Error&) +{ +} + +/** @page protocol + * @section loading Loading and Unloading Bundles + * + * The property ingen:loadedBundle on the engine can be used to load + * or unload bundles from Ingen's world. For example: + * + * @code{.ttl} + * # Load /old.lv2 + * [] + * a patch:Put ; + * patch:subject </> ; + * patch:body [ + * ingen:loadedBundle <file:///old.lv2/> + * ] . + * + * # Replace /old.lv2 with /new.lv2 + * [] + * a patch:Patch ; + * patch:subject </> ; + * patch:remove [ + * ingen:loadedBundle <file:///old.lv2/> + * ]; + * patch:add [ + * ingen:loadedBundle <file:///new.lv2/> + * ] . + * @endcode + */ + +} // namespace Ingen diff --git a/src/ClashAvoider.cpp b/src/ClashAvoider.cpp new file mode 100644 index 00000000..ae4438a4 --- /dev/null +++ b/src/ClashAvoider.cpp @@ -0,0 +1,136 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cstdio> +#include <sstream> +#include <string> +#include <utility> + +#include "ingen/ClashAvoider.hpp" +#include "ingen/Store.hpp" +#include "ingen/paths.hpp" + +namespace Ingen { + +ClashAvoider::ClashAvoider(const Store& store) + : _store(store) +{} + +const URI +ClashAvoider::map_uri(const URI& in) +{ + if (uri_is_path(in)) { + return path_to_uri(map_path(uri_to_path(in))); + } else { + return in; + } +} + +const Raul::Path +ClashAvoider::map_path(const Raul::Path& in) +{ + unsigned offset = 0; + bool has_offset = false; + const size_t pos = in.find_last_of('_'); + if (pos != std::string::npos && pos != (in.length()-1)) { + const std::string trailing = in.substr(pos + 1); + has_offset = (sscanf(trailing.c_str(), "%u", &offset) > 0); + } + + // Path without _n suffix + std::string base_path_str = in; + if (has_offset) { + base_path_str = base_path_str.substr(0, base_path_str.find_last_of('_')); + } + + Raul::Path base_path(base_path_str); + + auto m = _symbol_map.find(in); + if (m != _symbol_map.end()) { + return m->second; + } else { + typedef std::pair<SymbolMap::iterator, bool> InsertRecord; + + // See if parent is mapped + Raul::Path parent = in.parent(); + do { + auto p = _symbol_map.find(parent); + if (p != _symbol_map.end()) { + const Raul::Path mapped = Raul::Path( + p->second.base() + in.substr(parent.base().length())); + InsertRecord i = _symbol_map.emplace(in, mapped); + return i.first->second; + } + parent = parent.parent(); + } while (!parent.is_root()); + + if (!exists(in) && _symbol_map.find(in) == _symbol_map.end()) { + // No clash, use symbol unmodified + InsertRecord i = _symbol_map.emplace(in, in); + assert(i.second); + return i.first->second; + + } else { + // Append _2 _3 etc until an unused symbol is found + while (true) { + auto o = _offsets.find(base_path); + if (o != _offsets.end()) { + offset = ++o->second; + } else { + std::string parent_str = in.parent().base(); + parent_str = parent_str.substr(0, parent_str.find_last_of("/")); + if (parent_str.empty()) { + parent_str = "/"; + } + } + + if (offset == 0) { + offset = 2; + } + + std::stringstream ss; + ss << base_path << "_" << offset; + if (!exists(Raul::Path(ss.str()))) { + std::string name = base_path.symbol(); + if (name == "") { + name = "_"; + } + Raul::Symbol sym(name); + std::string str = ss.str(); + InsertRecord i = _symbol_map.emplace(in, Raul::Path(str)); + offset = _store.child_name_offset(in.parent(), sym, false); + _offsets.emplace(base_path, offset); + return i.first->second; + } else { + if (o != _offsets.end()) { + offset = ++o->second; + } else { + ++offset; + } + } + } + } + } +} + +bool +ClashAvoider::exists(const Raul::Path& path) const +{ + return _store.find(path) != _store.end(); +} + +} // namespace Ingen diff --git a/src/ColorContext.cpp b/src/ColorContext.cpp new file mode 100644 index 00000000..23b568f1 --- /dev/null +++ b/src/ColorContext.cpp @@ -0,0 +1,44 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/ColorContext.hpp" +#include "ingen_config.h" + +#ifdef HAVE_ISATTY +# include <unistd.h> +#else +inline int isatty(int fd) { return 0; } +#endif + +namespace Ingen { + +ColorContext::ColorContext(FILE* stream, Color color) + : _stream(stream) +{ + if (isatty(fileno(_stream))) { + fprintf(_stream, "\033[0;%dm", (int)color); + } +} + +ColorContext::~ColorContext() +{ + if (isatty(fileno(_stream))) { + fprintf(_stream, "\033[0m"); + fflush(_stream); + } +} + +} // namespace Ingen diff --git a/src/Configuration.cpp b/src/Configuration.cpp new file mode 100644 index 00000000..c797cf93 --- /dev/null +++ b/src/Configuration.cpp @@ -0,0 +1,386 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cerrno> +#include <cstring> +#include <iostream> +#include <thread> + +#include "ingen/Configuration.hpp" +#include "ingen/Forge.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/filesystem.hpp" +#include "ingen/ingen.h" +#include "ingen/runtime_paths.hpp" +#include "sord/sordmm.hpp" +#include "sratom/sratom.h" + +namespace Ingen { + +Configuration::Configuration(Forge& forge) + : _forge(forge) + , _shortdesc("A realtime modular audio processor.") + , _desc( + "Ingen is a flexible modular system that be used in various ways.\n" + "The engine can run as a server controlled via a network protocol,\n" + "as an LV2 plugin, or in a monolithic process with a GUI. The GUI\n" + "may be run separately to control a remote engine, and many clients\n" + "may connect to an engine at once.\n\n" + "Examples:\n" + " ingen -e # Run engine, listen for connections\n" + " ingen -g # Run GUI, connect to running engine\n" + " ingen -eg # Run engine and GUI in one process\n" + " ingen -eg foo.ingen # Run engine and GUI and load a graph") + , _max_name_length(0) +{ + add("atomicBundles", "atomic-bundles", 'a', "Execute bundles atomically", GLOBAL, forge.Bool, forge.make(false)); + add("bufferSize", "buffer-size", 'b', "Buffer size in samples", GLOBAL, forge.Int, forge.make(1024)); + add("clientPort", "client-port", 'C', "Client port", GLOBAL, forge.Int, Atom()); + add("connect", "connect", 'c', "Connect to engine URI", SESSION, forge.String, forge.alloc("unix:///tmp/ingen.sock")); + add("engine", "engine", 'e', "Run (JACK) engine", SESSION, forge.Bool, forge.make(false)); + add("enginePort", "engine-port", 'E', "Engine listen port", GLOBAL, forge.Int, forge.make(16180)); + add("socket", "socket", 'S', "Engine socket path", GLOBAL, forge.String, forge.alloc("/tmp/ingen.sock")); + add("gui", "gui", 'g', "Launch the GTK graphical interface", SESSION, forge.Bool, forge.make(false)); + add("", "help", 'h', "Print this help message", SESSION, forge.Bool, forge.make(false)); + add("", "version", 'V', "Print version information", SESSION, forge.Bool, forge.make(false)); + add("jackName", "jack-name", 'n', "JACK name", GLOBAL, forge.String, forge.alloc("ingen")); + add("jackServer", "jack-server", 's', "JACK server name", GLOBAL, forge.String, forge.alloc("")); + add("uuid", "uuid", 'u', "JACK session UUID", GLOBAL, forge.String, Atom()); + add("load", "load", 'l', "Load graph", SESSION, forge.String, Atom()); + add("serverLoad", "server-load", 'i', "Load graph (server side)", SESSION, forge.String, Atom()); + add("save", "save", 'o', "Save graph", SESSION, forge.String, Atom()); + add("execute", "execute", 'x', "File of commands to execute", SESSION, forge.String, Atom()); + add("path", "path", 'L', "Target path for loaded graph", SESSION, forge.String, Atom()); + add("queueSize", "queue-size", 'q', "Event queue size", GLOBAL, forge.Int, forge.make(4096)); + add("flushLog", "flush-log", 'f', "Flush logs after every entry", GLOBAL, forge.Bool, forge.make(false)); + add("dump", "dump", 'd', "Print debug output", SESSION, forge.Bool, forge.make(false)); + add("trace", "trace", 't', "Show LV2 plugin trace messages", SESSION, forge.Bool, forge.make(false)); + add("threads", "threads", 'p', "Number of processing threads", GLOBAL, forge.Int, forge.make(int32_t(std::max(std::thread::hardware_concurrency(), 1U)))); + add("humanNames", "human-names", 0, "Show human names in GUI", GUI, forge.Bool, forge.make(true)); + add("portLabels", "port-labels", 0, "Show port labels in GUI", GUI, forge.Bool, forge.make(true)); + add("graphDirectory", "graph-directory", 0, "Default directory for opening graphs", GUI, forge.String, Atom()); +} + +Configuration& +Configuration::add(const std::string& key, + const std::string& name, + char letter, + const std::string& desc, + Scope scope, + const LV2_URID type, + const Atom& value) +{ + assert(value.type() == type || value.type() == 0); + _max_name_length = std::max(_max_name_length, name.length()); + _options.emplace(name, Option{key, name, letter, desc, scope, type, value}); + if (!key.empty()) { + _keys.emplace(key, name); + } + if (letter != '\0') { + _short_names.emplace(letter, name); + } + return *this; +} + +std::string +Configuration::variable_string(LV2_URID type) const +{ + if (type == _forge.String) { + return "=STRING"; + } else if (type == _forge.Int) { + return "=INT"; + } + return ""; +} + +void +Configuration::print_usage(const std::string& program, std::ostream& os) +{ + os << "Usage: " << program << " [OPTION]... [GRAPH]" << std::endl; + os << _shortdesc << std::endl << std::endl; + os << _desc << std::endl << std::endl; + os << "Options:" << std::endl; + for (const auto& o : _options) { + const Option& option = o.second; + os << " "; + if (option.letter != '\0') { + os << "-" << option.letter << ", "; + } else { + os << " "; + } + os.width(_max_name_length + 11); + os << std::left; + os << (std::string("--") + o.first + variable_string(option.type)); + os << option.desc << std::endl; + } +} + +int +Configuration::set_value_from_string(Configuration::Option& option, + const std::string& value) +{ + if (option.type == _forge.Int) { + char* endptr = nullptr; + int intval = static_cast<int>(strtol(value.c_str(), &endptr, 10)); + if (endptr && *endptr == '\0') { + option.value = _forge.make(intval); + } else { + throw OptionError( + (fmt("Option `%1%' has non-integer value `%2%'") + % option.name % value).str()); + } + } else if (option.type == _forge.String) { + option.value = _forge.alloc(value.c_str()); + assert(option.value.type() == _forge.String); + } else if (option.type == _forge.Bool) { + option.value = _forge.make(bool(!strcmp(value.c_str(), "true"))); + assert(option.value.type() == _forge.Bool); + } else { + throw OptionError( + (fmt("Bad option type `%1%'") % option.name).str()); + } + return EXIT_SUCCESS; +} + +/** Parse command line arguments. */ +void +Configuration::parse(int argc, char** argv) +{ + for (int i = 1; i < argc; ++i) { + if (argv[i][0] != '-' || !strcmp(argv[i], "-")) { + // File argument + const Options::iterator o = _options.find("load"); + if (!o->second.value.is_valid()) { + _options.find("load")->second.value = _forge.alloc(argv[i]); + } else { + throw OptionError("Multiple graphs specified"); + } + } else if (argv[i][1] == '-') { + // Long option + std::string name = std::string(argv[i]).substr(2); + const char* equals = strchr(argv[i], '='); + if (equals) { + name = name.substr(0, name.find('=')); + } + + const Options::iterator o = _options.find(name); + if (o == _options.end()) { + throw OptionError( + (fmt("Unrecognized option `%1%'") % name).str()); + } else if (o->second.type == _forge.Bool) { // --flag + o->second.value = _forge.make(true); + } else if (equals) { // --opt=val + set_value_from_string(o->second, equals + 1); + } else if (++i < argc) { // --opt val + set_value_from_string(o->second, argv[i]); + } else { + throw OptionError( + (fmt("Missing value for `%1%'") % name).str()); + } + } else { + // Short option + const size_t len = strlen(argv[i]); + for (size_t j = 1; j < len; ++j) { + const char letter = argv[i][j]; + const ShortNames::iterator n = _short_names.find(letter); + if (n == _short_names.end()) { + throw OptionError( + (fmt("Unrecognized option `%1%'") % letter).str()); + } + + const Options::iterator o = _options.find(n->second); + if (j < len - 1) { // Non-final POSIX style flag + if (o->second.type != _forge.Bool) { + throw OptionError( + (fmt("Missing value for `%1%'") % letter).str()); + } + o->second.value = _forge.make(true); + } else if (o->second.type == _forge.Bool) { // -f + o->second.value = _forge.make(true); + } else if (++i < argc) { // -v val + set_value_from_string(o->second, argv[i]); + } else { + throw OptionError( + (fmt("Missing value for `%1%'") % letter).str()); + } + } + } + } +} + +bool +Configuration::load(const FilePath& path) +{ + if (!filesystem::exists(path)) { + return false; + } + + SerdNode node = serd_node_new_file_uri( + (const uint8_t*)path.c_str(), nullptr, nullptr, true); + const std::string uri((const char*)node.buf); + + Sord::World world; + Sord::Model model(world, uri, SORD_SPO, false); + SerdEnv* env = serd_env_new(&node); + model.load_file(env, SERD_TURTLE, uri, uri); + + Sord::Node nodemm(world, Sord::Node::URI, (const char*)node.buf); + Sord::Node nil; + for (Sord::Iter i = model.find(nodemm, nil, nil); !i.end(); ++i) { + const Sord::Node& pred = i.get_predicate(); + const Sord::Node& obj = i.get_object(); + if (pred.to_string().substr(0, sizeof(INGEN_NS) - 1) == INGEN_NS) { + const std::string key = pred.to_string().substr(sizeof(INGEN_NS) - 1); + const Keys::iterator k = _keys.find(key); + if (k != _keys.end() && obj.type() == Sord::Node::LITERAL) { + set_value_from_string(_options.find(k->second)->second, + obj.to_string()); + } + } + } + + serd_node_free(&node); + serd_env_free(env); + return true; +} + +FilePath +Configuration::save(URIMap& uri_map, + const std::string& app, + const FilePath& filename, + unsigned scopes) +{ + // Save to file if it is absolute, otherwise save to user config dir + FilePath path = filename; + if (!path.is_absolute()) { + path = FilePath(user_config_dir()) / app / filename; + } + + // Create parent directories if necessary + const FilePath dir = path.parent_path(); + if (!filesystem::create_directories(dir)) { + throw FileError((fmt("Error creating directory %1% (%2%)") + % dir % strerror(errno)).str()); + } + + // Attempt to open file for writing + FILE* file = fopen(path.c_str(), "w"); + if (!file) { + throw FileError((fmt("Failed to open file %1% (%2%)") + % path % strerror(errno)).str()); + } + + // Use the file's URI as the base URI + SerdURI base_uri; + SerdNode base = serd_node_new_file_uri( + (const uint8_t*)path.c_str(), nullptr, &base_uri, true); + + // Create environment with ingen prefix + SerdEnv* env = serd_env_new(&base); + serd_env_set_prefix_from_strings( + env, (const uint8_t*)"ingen", (const uint8_t*)INGEN_NS); + + // Create Turtle writer + SerdWriter* writer = serd_writer_new( + SERD_TURTLE, + (SerdStyle)(SERD_STYLE_RESOLVED|SERD_STYLE_ABBREVIATED), + env, + &base_uri, + serd_file_sink, + file); + + // Write a prefix directive for each prefix in the environment + serd_env_foreach(env, (SerdPrefixSink)serd_writer_set_prefix, writer); + + // Create an atom serialiser and connect it to the Turtle writer + Sratom* sratom = sratom_new(&uri_map.urid_map_feature()->urid_map); + sratom_set_pretty_numbers(sratom, true); + sratom_set_sink(sratom, (const char*)base.buf, + (SerdStatementSink)serd_writer_write_statement, nullptr, + writer); + + // Write a statement for each valid option + for (auto o : _options) { + const Atom& value = o.second.value; + if (!(o.second.scope & scopes) || + o.second.key.empty() || + !value.is_valid()) { + continue; + } + + const std::string key(std::string("ingen:") + o.second.key); + SerdNode pred = serd_node_from_string( + SERD_CURIE, (const uint8_t*)key.c_str()); + sratom_write(sratom, &uri_map.urid_unmap_feature()->urid_unmap, 0, + &base, &pred, value.type(), value.size(), value.get_body()); + } + + sratom_free(sratom); + serd_writer_free(writer); + serd_env_free(env); + serd_node_free(&base); + fclose(file); + + return path; +} + +std::list<FilePath> +Configuration::load_default(const std::string& app, const FilePath& filename) +{ + std::list<FilePath> loaded; + + const std::vector<FilePath> dirs = system_config_dirs(); + for (const auto& d : dirs) { + const FilePath path = d / app / filename; + if (load(path)) { + loaded.push_back(path); + } + } + + const FilePath path = user_config_dir() / app / filename; + if (load(path)) { + loaded.push_back(path); + } + + return loaded; +} + +const Atom& +Configuration::option(const std::string& long_name) const +{ + static const Atom nil; + auto o = _options.find(long_name); + if (o == _options.end()) { + return nil; + } else { + return o->second.value; + } +} + +bool +Configuration::set(const std::string& long_name, const Atom& value) +{ + auto o = _options.find(long_name); + if (o != _options.end()) { + o->second.value = value; + return true; + } + return false; +} + +} // namespace Ingen diff --git a/src/FilePath.cpp b/src/FilePath.cpp new file mode 100644 index 00000000..557fabc9 --- /dev/null +++ b/src/FilePath.cpp @@ -0,0 +1,242 @@ +/* + This file is part of Ingen. + Copyright 2018 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/FilePath.hpp" + +namespace Ingen { + +template <typename Char> +static bool +is_sep(const Char chr) +{ +#ifdef USE_WINDOWS_FILE_PATHS + return chr == L'/' || chr == preferred_separator; +#else + return chr == '/'; +#endif +} + +FilePath& +FilePath::operator=(FilePath&& path) noexcept +{ + _str = std::move(path._str); + path.clear(); + return *this; +} + +FilePath& +FilePath::operator=(string_type&& str) +{ + return *this = FilePath(std::move(str)); +} + +FilePath& +FilePath::operator/=(const FilePath& path) +{ + const FilePath::string_type str = path.string(); + if (!_str.empty() && !is_sep(_str.back()) && !str.empty() && + !is_sep(str.front())) { + _str += preferred_separator; + } + + _str += str; + return *this; +} + +FilePath& +FilePath::operator+=(const FilePath& path) +{ + return operator+=(path.native()); +} + +FilePath& +FilePath::operator+=(const string_type& str) +{ + _str += str; + return *this; +} + +FilePath& +FilePath::operator+=(const value_type* str) +{ + _str += str; + return *this; +} + +FilePath& +FilePath::operator+=(value_type chr) +{ + _str += chr; + return *this; +} + +FilePath& +FilePath::operator+=(boost::basic_string_view<value_type> sv) +{ + _str.append(sv.data(), sv.size()); + return *this; +} + +FilePath +FilePath::root_name() const +{ +#ifdef USE_WINDOWS_FILE_PATHS + if (_str.length() >= 2 && _str[0] >= 'A' && _str[0] <= 'Z' && + _str[1] == ':') { + return FilePath(_str.substr(0, 2)); + } +#endif + + return FilePath(); +} + +FilePath +FilePath::root_directory() const +{ +#ifdef USE_WINDOWS_FILE_PATHS + const auto name = root_name().string(); + return name.empty() ? Path() : Path(name + preferred_separator); +#endif + + return _str[0] == '/' ? FilePath("/") : FilePath(); +} + +FilePath +FilePath::root_path() const +{ +#ifdef USE_WINDOWS_FILE_PATHS + const auto name = root_name(); + return name.empty() ? FilePath() : name / root_directory(); +#endif + return root_directory(); +} + +FilePath +FilePath::relative_path() const +{ + const auto root = root_path(); + return root.empty() ? FilePath() + : FilePath(_str.substr(root.string().length())); +} + +FilePath +FilePath::parent_path() const +{ + if (empty() || *this == root_path()) { + return *this; + } + + const auto first_sep = find_first_sep(); + const auto last_sep = find_last_sep(); + return ((last_sep == std::string::npos || last_sep == first_sep) + ? root_path() + : FilePath(_str.substr(0, last_sep))); +} + +FilePath +FilePath::filename() const +{ + return ((empty() || *this == root_path()) + ? FilePath() + : FilePath(_str.substr(find_last_sep() + 1))); +} + +FilePath +FilePath::stem() const +{ + const auto name = filename(); + const auto dot = name.string().find('.'); + return ((dot == std::string::npos) ? name + : FilePath(name.string().substr(0, dot))); +} + +FilePath +FilePath::extension() const +{ + const auto name = filename().string(); + const auto dot = name.find('.'); + return ((dot == std::string::npos) ? FilePath() + : FilePath(name.substr(dot, dot))); +} + +bool +FilePath::is_absolute() const +{ +#ifdef USE_WINDOWS_FILE_PATHS + return !root_name().empty(); +#else + return !root_directory().empty(); +#endif +} + +std::size_t +FilePath::find_first_sep() const +{ + const auto i = std::find_if(_str.begin(), _str.end(), is_sep<value_type>); + return i == _str.end() ? std::string::npos : (i - _str.begin()); +} + +std::size_t +FilePath::find_last_sep() const +{ + const auto i = std::find_if(_str.rbegin(), _str.rend(), is_sep<value_type>); + return (i == _str.rend() ? std::string::npos + : (_str.length() - 1 - (i - _str.rbegin()))); +} + +bool +operator==(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return lhs.string() == rhs.string(); +} + +bool +operator!=(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return !(lhs == rhs); +} + +bool +operator<(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return lhs.string().compare(rhs.string()) < 0; +} + +bool +operator<=(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return !(rhs < lhs); +} + +bool +operator>(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return rhs < lhs; +} + +bool +operator>=(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return !(lhs < rhs); +} + +FilePath +operator/(const FilePath& lhs, const FilePath& rhs) +{ + return FilePath(lhs) /= rhs; +} + +} // namespace Ingen diff --git a/src/Forge.cpp b/src/Forge.cpp new file mode 100644 index 00000000..688d994d --- /dev/null +++ b/src/Forge.cpp @@ -0,0 +1,69 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <sstream> + +#include "ingen/Forge.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" + +namespace Ingen { + +Forge::Forge(URIMap& map) + : _map(map) +{ + lv2_atom_forge_init(this, &map.urid_map_feature()->urid_map); +} + +Atom +Forge::make_urid(const Ingen::URI& u) +{ + const LV2_URID urid = _map.map_uri(u.string()); + return Atom(sizeof(int32_t), URID, &urid); +} + +std::string +Forge::str(const Atom& atom, bool quoted) +{ + std::ostringstream ss; + if (atom.type() == Int) { + ss << atom.get<int32_t>(); + } else if (atom.type() == Float) { + ss << atom.get<float>(); + } else if (atom.type() == Bool) { + ss << (atom.get<int32_t>() ? "true" : "false"); + } else if (atom.type() == URI) { + ss << (quoted ? "<" : "") + << atom.ptr<const char>() + << (quoted ? ">" : ""); + } else if (atom.type() == URID) { + ss << (quoted ? "<" : "") + << _map.unmap_uri(atom.get<int32_t>()) + << (quoted ? ">" : ""); + } else if (atom.type() == Path) { + ss << (quoted ? "<" : "") + << atom.ptr<const char>() + << (quoted ? ">" : ""); + } else if (atom.type() == String) { + ss << (quoted ? "\"" : "") + << atom.ptr<const char>() + << (quoted ? "\"" : ""); + } + return ss.str(); +} + +} // namespace Ingen diff --git a/src/LV2Features.cpp b/src/LV2Features.cpp new file mode 100644 index 00000000..356df42c --- /dev/null +++ b/src/LV2Features.cpp @@ -0,0 +1,83 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> + +#include "ingen/LV2Features.hpp" + +namespace Ingen { + +void +LV2Features::Feature::free_feature(LV2_Feature* feature) +{ + free(feature->data); + free(feature); +} + +LV2Features::LV2Features() +{ +} + +void +LV2Features::add_feature(SPtr<Feature> feature) +{ + _features.push_back(feature); +} + +LV2Features::FeatureArray::FeatureArray(FeatureVector& features) + : _features(features) +{ + _array = (LV2_Feature**)malloc(sizeof(LV2_Feature*) * (features.size() + 1)); + _array[features.size()] = nullptr; + for (size_t i = 0; i < features.size(); ++i) { + _array[i] = features[i].get(); + } +} + +LV2Features::FeatureArray::~FeatureArray() +{ + free(_array); +} + +bool +LV2Features::is_supported(const std::string& uri) const +{ + if (uri == "http://lv2plug.in/ns/lv2core#isLive") { + return true; + } + + for (const auto& f : _features) { + if (f->uri() == uri) { + return true; + } + } + return false; +} + +SPtr<LV2Features::FeatureArray> +LV2Features::lv2_features(World* world, Node* node) const +{ + FeatureArray::FeatureVector vec; + for (const auto& f : _features) { + SPtr<LV2_Feature> fptr = f->feature(world, node); + if (fptr) { + vec.push_back(fptr); + } + } + return SPtr<FeatureArray>(new FeatureArray(vec)); +} + +} // namespace Ingen diff --git a/src/Library.cpp b/src/Library.cpp new file mode 100644 index 00000000..148b27d0 --- /dev/null +++ b/src/Library.cpp @@ -0,0 +1,56 @@ +/* + This file is part of Ingen. + Copyright 2018 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Library.hpp" + +#ifdef _WIN32 +# include <windows.h> +# define dlopen(path, flags) LoadLibrary(path) +# define dlclose(lib) FreeLibrary((HMODULE)lib) +# define dlerror() "unknown error" +#else +# include <dlfcn.h> +#endif + +namespace Ingen { + +Library::Library(const FilePath& path) : _lib(dlopen(path.c_str(), RTLD_NOW)) +{} + +Library::~Library() +{ + dlclose(_lib); +} + +Library::VoidFuncPtr +Library::get_function(const char* name) +{ +#ifdef _WIN32 + return (VoidFuncPtr)GetProcAddress((HMODULE)_lib, name); +#else + typedef VoidFuncPtr (*VoidFuncGetter)(void*, const char*); + VoidFuncGetter dlfunc = (VoidFuncGetter)dlsym; + return dlfunc(_lib, name); +#endif +} + +const char* +Library::get_last_error() +{ + return dlerror(); +} + +} // namespace Ingen diff --git a/src/Log.cpp b/src/Log.cpp new file mode 100644 index 00000000..6145bcd1 --- /dev/null +++ b/src/Log.cpp @@ -0,0 +1,162 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdio> + +#include "ingen/Log.hpp" +#include "ingen/Node.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/ColorContext.hpp" + +namespace Ingen { + +Log::Log(LV2_Log_Log* log, URIs& uris) + : _log(log) + , _uris(uris) + , _flush(false) + , _trace(false) +{} + +void +Log::rt_error(const char* msg) +{ +#ifndef NDEBUG + va_list args; + vtprintf(_uris.log_Error, msg, args); +#endif +} + +void +Log::error(const std::string& msg) +{ + va_list args; + vtprintf(_uris.log_Error, msg.c_str(), args); +} + +void +Log::warn(const std::string& msg) +{ + va_list args; + vtprintf(_uris.log_Warning, msg.c_str(), args); +} + +void +Log::info(const std::string& msg) +{ + va_list args; + vtprintf(_uris.log_Note, msg.c_str(), args); +} + +void +Log::trace(const std::string& msg) +{ + va_list args; + vtprintf(_uris.log_Trace, msg.c_str(), args); +} + +void +Log::print(FILE* stream, const std::string& msg) +{ + fprintf(stream, "%s", msg.c_str()); + if (_flush) { + fflush(stdout); + } +} + +int +Log::vtprintf(LV2_URID type, const char* fmt, va_list args) +{ + int ret = 0; + if (type == _uris.log_Trace && !_trace) { + return 0; + } else if (_sink) { + _sink(type, fmt, args); + } + + if (_log) { + ret = _log->vprintf(_log->handle, type, fmt, args); + } else if (type == _uris.log_Error) { + ColorContext ctx(stderr, ColorContext::Color::RED); + ret = vfprintf(stderr, fmt, args); + } else if (type == _uris.log_Warning) { + ColorContext ctx(stderr, ColorContext::Color::YELLOW); + ret = vfprintf(stderr, fmt, args); + } else if (type == _uris.log_Note) { + ColorContext ctx(stderr, ColorContext::Color::GREEN); + ret = vfprintf(stdout, fmt, args); + } else if (_trace && type == _uris.log_Trace) { + ColorContext ctx(stderr, ColorContext::Color::GREEN); + ret = vfprintf(stderr, fmt, args); + } else { + fprintf(stderr, "Unknown log type %d\n", type); + return 0; + } + if (_flush) { + fflush(stdout); + } + return ret; +} + +static int +log_vprintf(LV2_Log_Handle handle, LV2_URID type, const char* fmt, va_list args) +{ + Log::Feature::Handle* f = (Log::Feature::Handle*)handle; + va_list noargs; + + int ret = f->log->vtprintf(type, f->node->path().c_str(), noargs); + ret += f->log->vtprintf(type, ": ", noargs); + ret += f->log->vtprintf(type, fmt, args); + + return ret; +} + +static int +log_printf(LV2_Log_Handle handle, LV2_URID type, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + const int ret = log_vprintf(handle, type, fmt, args); + va_end(args); + + return ret; +} + +static void +free_log_feature(LV2_Feature* feature) { + LV2_Log_Log* lv2_log = (LV2_Log_Log*)feature->data; + free(lv2_log->handle); + free(feature); +} + +SPtr<LV2_Feature> +Log::Feature::feature(World* world, Node* block) +{ + Handle* handle = (Handle*)calloc(1, sizeof(Handle)); + handle->lv2_log.handle = handle; + handle->lv2_log.printf = log_printf; + handle->lv2_log.vprintf = log_vprintf; + handle->log = &world->log(); + handle->node = block; + + LV2_Feature* f = (LV2_Feature*)malloc(sizeof(LV2_Feature)); + f->URI = LV2_LOG__log; + f->data = &handle->lv2_log; + + return SPtr<LV2_Feature>(f, &free_log_feature); +} + +} // namespace Ingen diff --git a/src/Parser.cpp b/src/Parser.cpp new file mode 100644 index 00000000..dace07ed --- /dev/null +++ b/src/Parser.cpp @@ -0,0 +1,713 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <map> +#include <set> +#include <string> +#include <utility> + +#include "ingen/Atom.hpp" +#include "ingen/AtomForgeSink.hpp" +#include "ingen/Forge.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Parser.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/filesystem.hpp" +#include "ingen/paths.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "serd/serd.h" +#include "sord/sordmm.hpp" +#include "sratom/sratom.h" + +#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define NS_RDFS "http://www.w3.org/2000/01/rdf-schema#" + +namespace Ingen { + +std::set<Parser::ResourceRecord> +Parser::find_resources(Sord::World& world, + const URI& manifest_uri, + const URI& type_uri) +{ + const Sord::URI base (world, manifest_uri.string()); + const Sord::URI type (world, type_uri.string()); + const Sord::URI rdf_type (world, NS_RDF "type"); + const Sord::URI rdfs_seeAlso(world, NS_RDFS "seeAlso"); + const Sord::Node nil; + + SerdEnv* env = serd_env_new(sord_node_to_serd_node(base.c_obj())); + Sord::Model model(world, manifest_uri.string()); + model.load_file(env, SERD_TURTLE, manifest_uri.string()); + + std::set<ResourceRecord> resources; + for (Sord::Iter i = model.find(nil, rdf_type, type); !i.end(); ++i) { + const Sord::Node resource = i.get_subject(); + const std::string resource_uri = resource.to_c_string(); + std::string file_path = ""; + Sord::Iter f = model.find(resource, rdfs_seeAlso, nil); + if (!f.end()) { + uint8_t* p = serd_file_uri_parse(f.get_object().to_u_string(), nullptr); + file_path = (const char*)p; + serd_free(p); + } + resources.insert(ResourceRecord(resource, file_path)); + } + + serd_env_free(env); + return resources; +} + +static boost::optional<Raul::Path> +get_path(const URI base, const URI uri) +{ + const URI relative = uri.make_relative(base); + const std::string uri_str = "/" + relative.string(); + return Raul::Path::is_valid(uri_str) ? Raul::Path(uri_str) + : boost::optional<Raul::Path>(); +} + +static bool +skip_property(Ingen::URIs& uris, const Sord::Node& predicate) +{ + return (predicate == INGEN__file || + predicate == uris.ingen_arc || + predicate == uris.ingen_block || + predicate == uris.lv2_port); +} + +static Properties +get_properties(Ingen::World* world, + Sord::Model& model, + const Sord::Node& subject, + Resource::Graph ctx) +{ + LV2_URID_Map* map = &world->uri_map().urid_map_feature()->urid_map; + Sratom* sratom = sratom_new(map); + + LV2_Atom_Forge forge; + lv2_atom_forge_init(&forge, map); + + AtomForgeSink out(&forge); + + const Sord::Node nil; + Properties props; + for (Sord::Iter i = model.find(subject, nil, nil); !i.end(); ++i) { + if (!skip_property(world->uris(), i.get_predicate())) { + out.clear(); + sratom_read(sratom, &forge, world->rdf_world()->c_obj(), + model.c_obj(), i.get_object().c_obj()); + const LV2_Atom* atom = out.atom(); + Atom atomm; + atomm = world->forge().alloc( + atom->size, atom->type, LV2_ATOM_BODY_CONST(atom)); + props.emplace(i.get_predicate(), Property(atomm, ctx)); + } + } + + sratom_free(sratom); + return props; +} + +typedef std::pair<Raul::Path, Properties> PortRecord; + +static boost::optional<PortRecord> +get_port(Ingen::World* world, + Sord::Model& model, + const Sord::Node& subject, + Resource::Graph ctx, + const Raul::Path& parent, + uint32_t* index) +{ + const URIs& uris = world->uris(); + + // Get all properties + Properties props = get_properties(world, model, subject, ctx); + + // Get index if requested (for Graphs) + if (index) { + Properties::const_iterator i = props.find(uris.lv2_index); + if (i == props.end() + || i->second.type() != world->forge().Int + || i->second.get<int32_t>() < 0) { + world->log().error(fmt("Port %1% has no valid index\n") % subject); + return boost::optional<PortRecord>(); + } + *index = i->second.get<int32_t>(); + } + + // Get symbol + Properties::const_iterator s = props.find(uris.lv2_symbol); + std::string sym; + if (s != props.end() && s->second.type() == world->forge().String) { + sym = s->second.ptr<char>(); + } else { + const std::string subject_str = subject.to_string(); + const size_t last_slash = subject_str.find_last_of("/"); + + sym = ((last_slash == std::string::npos) + ? subject_str + : subject_str.substr(last_slash + 1)); + } + + if (!Raul::Symbol::is_valid(sym)) { + world->log().error(fmt("Port %1% has invalid symbol `%2%'\n") + % subject % sym); + return boost::optional<PortRecord>(); + } + + const Raul::Symbol port_sym(sym); + const Raul::Path port_path(parent.child(port_sym)); + + props.erase(uris.lv2_symbol); // Don't set symbol property in engine + return make_pair(port_path, props); +} + +static boost::optional<Raul::Path> +parse( + World* world, + Interface* target, + Sord::Model& model, + const URI& base_uri, + Sord::Node& subject, + boost::optional<Raul::Path> parent = boost::optional<Raul::Path>(), + boost::optional<Raul::Symbol> symbol = boost::optional<Raul::Symbol>(), + boost::optional<Properties> data = boost::optional<Properties>()); + +static boost::optional<Raul::Path> +parse_graph( + World* world, + Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + Resource::Graph ctx, + boost::optional<Raul::Path> parent = boost::optional<Raul::Path>(), + boost::optional<Raul::Symbol> symbol = boost::optional<Raul::Symbol>(), + boost::optional<Properties> data = boost::optional<Properties>()); + +static boost::optional<Raul::Path> +parse_block( + World* world, + Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& path, + boost::optional<Properties> data = boost::optional<Properties>()); + +static bool +parse_properties( + World* world, + Interface* target, + Sord::Model& model, + const Sord::Node& subject, + Resource::Graph ctx, + const URI& uri, + boost::optional<Properties> data = boost::optional<Properties>()); + +static bool +parse_arcs( + World* world, + Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& graph); + +static boost::optional<Raul::Path> +parse_block(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& path, + boost::optional<Properties> data) +{ + const URIs& uris = world->uris(); + + // Try lv2:prototype and old ingen:prototype for backwards compatibility + const Sord::URI prototype_predicates[] = { + Sord::URI(*world->rdf_world(), uris.lv2_prototype), + Sord::URI(*world->rdf_world(), uris.ingen_prototype) + }; + + // Get prototype + Sord::Node prototype; + for (const Sord::URI& pred : prototype_predicates) { + prototype = model.get(subject, pred, Sord::Node()); + if (prototype.is_valid()) { + break; + } + } + + if (!prototype.is_valid()) { + world->log().error( + fmt("Block %1% (%2%) missing mandatory lv2:prototype\n") % + subject % path); + return boost::optional<Raul::Path>(); + } + + const uint8_t* type_uri = (const uint8_t*)prototype.to_c_string(); + if (!serd_uri_string_has_scheme(type_uri) || + !strncmp((const char*)type_uri, "file:", 5)) { + // Prototype is a file, subgraph + SerdURI base_uri_parts; + serd_uri_parse((const uint8_t*)base_uri.c_str(), &base_uri_parts); + + SerdURI ignored; + SerdNode sub_uri = serd_node_new_uri_from_string( + type_uri, + &base_uri_parts, + &ignored); + + const std::string sub_uri_str = (const char*)sub_uri.buf; + const std::string sub_file = sub_uri_str + "/main.ttl"; + + const SerdNode sub_base = serd_node_from_string( + SERD_URI, (const uint8_t*)sub_file.c_str()); + + Sord::Model sub_model(*world->rdf_world(), sub_file); + SerdEnv* env = serd_env_new(&sub_base); + sub_model.load_file(env, SERD_TURTLE, sub_file); + serd_env_free(env); + + Sord::URI sub_node(*world->rdf_world(), sub_file); + parse_graph(world, target, sub_model, sub_base, + sub_node, Resource::Graph::INTERNAL, + path.parent(), Raul::Symbol(path.symbol())); + + parse_graph(world, target, model, base_uri, + subject, Resource::Graph::EXTERNAL, + path.parent(), Raul::Symbol(path.symbol())); + } else { + // Prototype is non-file URI, plugin + Properties props = get_properties( + world, model, subject, Resource::Graph::DEFAULT); + props.emplace(uris.rdf_type, uris.forge.make_urid(uris.ingen_Block)); + target->put(path_to_uri(path), props); + } + return path; +} + +static boost::optional<Raul::Path> +parse_graph(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + Resource::Graph ctx, + boost::optional<Raul::Path> parent, + boost::optional<Raul::Symbol> symbol, + boost::optional<Properties> data) +{ + const URIs& uris = world->uris(); + + const Sord::URI ingen_block(*world->rdf_world(), uris.ingen_block); + const Sord::URI lv2_port(*world->rdf_world(), LV2_CORE__port); + + const Sord::Node& graph = subject; + const Sord::Node nil; + + // Build graph path and symbol + Raul::Path graph_path; + if (parent && symbol) { + graph_path = parent->child(*symbol); + } else if (parent) { + graph_path = *parent; + } else { + graph_path = Raul::Path("/"); + } + + if (!symbol) { + symbol = Raul::Symbol("_"); + } + + // Create graph + Properties props = get_properties(world, model, subject, ctx); + target->put(path_to_uri(graph_path), props, ctx); + + // For each port on this graph + typedef std::map<uint32_t, PortRecord> PortRecords; + PortRecords ports; + for (Sord::Iter p = model.find(graph, lv2_port, nil); !p.end(); ++p) { + Sord::Node port = p.get_object(); + + // Get all properties + uint32_t index = 0; + boost::optional<PortRecord> port_record = get_port( + world, model, port, ctx, graph_path, &index); + if (!port_record) { + world->log().error(fmt("Invalid port %1%\n") % port); + return boost::optional<Raul::Path>(); + } + + // Store port information in ports map + if (ports.find(index) == ports.end()) { + ports[index] = *port_record; + } else { + world->log().error(fmt("Ignored port %1% with duplicate index %2%\n") + % port % index); + } + } + + // Create ports in order by index + for (const auto& p : ports) { + target->put(path_to_uri(p.second.first), + p.second.second, + ctx); + } + + if (ctx != Resource::Graph::INTERNAL) { + return graph_path; // Not parsing graph internals, finished now + } + + // For each block in this graph + for (Sord::Iter n = model.find(subject, ingen_block, nil); !n.end(); ++n) { + Sord::Node node = n.get_object(); + URI node_uri = node; + assert(!node_uri.path().empty() && node_uri.path() != "/"); + const Raul::Path block_path = graph_path.child( + Raul::Symbol(FilePath(node_uri.path()).stem().string())); + + // Parse and create block + parse_block(world, target, model, base_uri, node, block_path, + boost::optional<Properties>()); + + // For each port on this block + for (Sord::Iter p = model.find(node, lv2_port, nil); !p.end(); ++p) { + Sord::Node port = p.get_object(); + + Resource::Graph subctx = Resource::Graph::DEFAULT; + if (!model.find(node, + Sord::URI(*world->rdf_world(), uris.rdf_type), + Sord::URI(*world->rdf_world(), uris.ingen_Graph)).end()) { + subctx = Resource::Graph::EXTERNAL; + } + + // Get all properties + boost::optional<PortRecord> port_record = get_port( + world, model, port, subctx, block_path, nullptr); + if (!port_record) { + world->log().error(fmt("Invalid port %1%\n") % port); + return boost::optional<Raul::Path>(); + } + + // Create port and/or set all port properties + target->put(path_to_uri(port_record->first), + port_record->second, + subctx); + } + } + + // Now that all ports and blocks exist, create arcs inside graph + parse_arcs(world, target, model, base_uri, subject, graph_path); + + return graph_path; +} + +static bool +parse_arc(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& graph) +{ + const URIs& uris = world->uris(); + + const Sord::URI ingen_tail(*world->rdf_world(), uris.ingen_tail); + const Sord::URI ingen_head(*world->rdf_world(), uris.ingen_head); + const Sord::Node nil; + + Sord::Iter t = model.find(subject, ingen_tail, nil); + Sord::Iter h = model.find(subject, ingen_head, nil); + + if (t.end()) { + world->log().error("Arc has no tail\n"); + return false; + } else if (h.end()) { + world->log().error("Arc has no head\n"); + return false; + } + + const boost::optional<Raul::Path> tail_path = get_path( + base_uri, t.get_object()); + if (!tail_path) { + world->log().error("Arc tail has invalid URI\n"); + return false; + } + + const boost::optional<Raul::Path> head_path = get_path( + base_uri, h.get_object()); + if (!head_path) { + world->log().error("Arc head has invalid URI\n"); + return false; + } + + if (!(++t).end()) { + world->log().error("Arc has multiple tails\n"); + return false; + } else if (!(++h).end()) { + world->log().error("Arc has multiple heads\n"); + return false; + } + + target->connect(graph.child(*tail_path), graph.child(*head_path)); + + return true; +} + +static bool +parse_arcs(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& graph) +{ + const Sord::URI ingen_arc(*world->rdf_world(), world->uris().ingen_arc); + const Sord::Node nil; + + for (Sord::Iter i = model.find(subject, ingen_arc, nil); !i.end(); ++i) { + parse_arc(world, target, model, base_uri, i.get_object(), graph); + } + + return true; +} + +static bool +parse_properties(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const Sord::Node& subject, + Resource::Graph ctx, + const URI& uri, + boost::optional<Properties> data) +{ + Properties properties = get_properties(world, model, subject, ctx); + + target->put(uri, properties, ctx); + + // Set passed properties last to override any loaded values + if (data) { + target->put(uri, data.get(), ctx); + } + + return true; +} + +static boost::optional<Raul::Path> +parse(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + Sord::Node& subject, + boost::optional<Raul::Path> parent, + boost::optional<Raul::Symbol> symbol, + boost::optional<Properties> data) +{ + const URIs& uris = world->uris(); + + const Sord::URI graph_class (*world->rdf_world(), uris.ingen_Graph); + const Sord::URI block_class (*world->rdf_world(), uris.ingen_Block); + const Sord::URI arc_class (*world->rdf_world(), uris.ingen_Arc); + const Sord::URI internal_class(*world->rdf_world(), uris.ingen_Internal); + const Sord::URI in_port_class (*world->rdf_world(), LV2_CORE__InputPort); + const Sord::URI out_port_class(*world->rdf_world(), LV2_CORE__OutputPort); + const Sord::URI lv2_class (*world->rdf_world(), LV2_CORE__Plugin); + const Sord::URI rdf_type (*world->rdf_world(), uris.rdf_type); + const Sord::Node nil; + + // Parse explicit subject graph + if (subject.is_valid()) { + return parse_graph(world, target, model, base_uri, + subject, Resource::Graph::INTERNAL, + parent, symbol, data); + } + + // Get all subjects and their types (?subject a ?type) + typedef std::map< Sord::Node, std::set<Sord::Node> > Subjects; + Subjects subjects; + for (Sord::Iter i = model.find(subject, rdf_type, nil); !i.end(); ++i) { + const Sord::Node& subject = i.get_subject(); + const Sord::Node& rdf_class = i.get_object(); + + assert(rdf_class.is_uri()); + auto s = subjects.find(subject); + if (s == subjects.end()) { + std::set<Sord::Node> types; + types.insert(rdf_class); + subjects.emplace(subject, types); + } else { + s->second.insert(rdf_class); + } + } + + // Parse and create each subject + for (const auto& i : subjects) { + const Sord::Node& s = i.first; + const std::set<Sord::Node>& types = i.second; + boost::optional<Raul::Path> ret; + if (types.find(graph_class) != types.end()) { + ret = parse_graph(world, target, model, base_uri, + s, Resource::Graph::INTERNAL, + parent, symbol, data); + } else if (types.find(block_class) != types.end()) { + const Raul::Path rel_path(*get_path(base_uri, s)); + const Raul::Path path = parent ? parent->child(rel_path) : rel_path; + ret = parse_block(world, target, model, base_uri, s, path, data); + } else if (types.find(in_port_class) != types.end() || + types.find(out_port_class) != types.end()) { + const Raul::Path rel_path(*get_path(base_uri, s)); + const Raul::Path path = parent ? parent->child(rel_path) : rel_path; + parse_properties(world, target, model, + s, Resource::Graph::DEFAULT, + path_to_uri(path), data); + ret = path; + } else if (types.find(arc_class) != types.end()) { + Raul::Path parent_path(parent ? parent.get() : Raul::Path("/")); + parse_arc(world, target, model, base_uri, s, parent_path); + } else { + world->log().error("Subject has no known types\n"); + } + } + + return boost::optional<Raul::Path>(); +} + +bool +Parser::parse_file(Ingen::World* world, + Ingen::Interface* target, + const FilePath& path, + boost::optional<Raul::Path> parent, + boost::optional<Raul::Symbol> symbol, + boost::optional<Properties> data) +{ + // Get absolute file path + FilePath file_path = path; + if (!file_path.is_absolute()) { + file_path = filesystem::current_path() / file_path; + } + + // Find file to use as manifest + const bool is_bundle = filesystem::is_directory(file_path); + const FilePath manifest_path = + (is_bundle ? file_path / "manifest.ttl" : file_path); + + URI manifest_uri(manifest_path); + + // Find graphs in manifest + const std::set<ResourceRecord> resources = find_resources( + *world->rdf_world(), manifest_uri, URI(INGEN__Graph)); + + if (resources.empty()) { + world->log().error(fmt("No graphs found in %1%\n") % path); + return false; + } + + /* Choose the graph to load. If this is a manifest, then there should only be + one, but if this is a graph file, subgraphs will be returned as well. + In this case, choose the one with the file URI. */ + URI uri; + for (const ResourceRecord& r : resources) { + if (r.uri == URI(manifest_path)) { + uri = r.uri; + file_path = r.filename; + break; + } + } + + if (uri.empty()) { + // Didn't find a graph with the same URI as the file, use the first + uri = (*resources.begin()).uri; + file_path = (*resources.begin()).filename; + } + + if (file_path.empty()) { + // No seeAlso file, use manifest (probably the graph file itself) + file_path = manifest_path; + } + + // Initialise parsing environment + const URI file_uri = URI(file_path); + const uint8_t* uri_c_str = (const uint8_t*)uri.c_str(); + SerdNode base_node = serd_node_from_string(SERD_URI, uri_c_str); + SerdEnv* env = serd_env_new(&base_node); + + // Load graph into model + Sord::Model model(*world->rdf_world(), uri.string(), SORD_SPO|SORD_PSO, false); + model.load_file(env, SERD_TURTLE, file_uri); + serd_env_free(env); + + world->log().info(fmt("Loading %1% from %2%\n") % uri % file_path); + if (parent) { + world->log().info(fmt("Parent: %1%\n") % parent->c_str()); + } + if (symbol) { + world->log().info(fmt("Symbol: %1%\n") % symbol->c_str()); + } + + Sord::Node subject(*world->rdf_world(), Sord::Node::URI, uri.string()); + boost::optional<Raul::Path> parsed_path + = parse(world, target, model, model.base_uri(), + subject, parent, symbol, data); + + if (parsed_path) { + target->set_property(path_to_uri(*parsed_path), + URI(INGEN__file), + world->forge().alloc_uri(uri.string())); + return true; + } else { + world->log().warn("Document URI lost\n"); + return false; + } +} + +boost::optional<URI> +Parser::parse_string(Ingen::World* world, + Ingen::Interface* target, + const std::string& str, + const URI& base_uri, + boost::optional<Raul::Path> parent, + boost::optional<Raul::Symbol> symbol, + boost::optional<Properties> data) +{ + // Load string into model + Sord::Model model(*world->rdf_world(), base_uri, SORD_SPO|SORD_PSO, false); + + SerdEnv* env = serd_env_new(nullptr); + if (!base_uri.empty()) { + const SerdNode base = serd_node_from_string( + SERD_URI, (const uint8_t*)base_uri.c_str()); + serd_env_set_base_uri(env, &base); + } + model.load_string(env, SERD_TURTLE, str.c_str(), str.length(), base_uri); + + URI actual_base((const char*)serd_env_get_base_uri(env, nullptr)->buf); + serd_env_free(env); + + world->log().info(fmt("Parsing string (base %1%)\n") % base_uri); + + Sord::Node subject; + parse(world, target, model, actual_base, subject, parent, symbol, data); + return actual_base; +} + +} // namespace Ingen diff --git a/src/Resource.cpp b/src/Resource.cpp new file mode 100644 index 00000000..d0261eee --- /dev/null +++ b/src/Resource.cpp @@ -0,0 +1,234 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <utility> + +#include "ingen/Atom.hpp" +#include "ingen/Forge.hpp" +#include "ingen/Resource.hpp" +#include "ingen/URIs.hpp" + +namespace Ingen { + +bool +Resource::add_property(const URI& uri, const Atom& value, Graph ctx) +{ + // Ignore duplicate statements + typedef Properties::const_iterator iterator; + const std::pair<iterator, iterator> range = _properties.equal_range(uri); + for (iterator i = range.first; i != range.second && i != _properties.end(); ++i) { + if (i->second == value && i->second.context() == ctx) { + return false; + } + } + + if (uri != _uris.ingen_activity) { + // Insert new property + const Atom& v = _properties.emplace(uri, Property(value, ctx))->second; + on_property(uri, v); + } else { + // Announce ephemeral activity, but do not store + on_property(uri, value); + } + + return true; +} + +const Atom& +Resource::set_property(const URI& uri, const Atom& value, Resource::Graph ctx) +{ + // Erase existing property in this context + for (auto i = _properties.find(uri); + (i != _properties.end()) && (i->first == uri);) { + auto next = i; + ++next; + if (i->second.context() == ctx) { + const Atom value(i->second); + _properties.erase(i); + on_property_removed(uri, value); + } + i = next; + } + + if (uri != _uris.ingen_activity) { + // Insert new property + const Atom& v = _properties.emplace(uri, Property(value, ctx))->second; + on_property(uri, v); + return v; + } else { + // Announce ephemeral activity, but do not store + on_property(uri, value); + return value; + } +} + +const Atom& +Resource::set_property(const URI& uri, + const URIs::Quark& value, + Resource::Graph ctx) +{ + return set_property(uri, value.urid, ctx); +} + +void +Resource::remove_property(const URI& uri, const Atom& value) +{ + if (_uris.patch_wildcard == value) { + _properties.erase(uri); + } else { + for (auto i = _properties.find(uri); + i != _properties.end() && (i->first == uri); + ++i) { + if (i->second == value) { + _properties.erase(i); + break; + } + } + } + on_property_removed(uri, value); +} + +void +Resource::remove_property(const URI& uri, const URIs::Quark& value) +{ + remove_property(uri, value.urid); + remove_property(uri, value.uri); +} + +bool +Resource::has_property(const URI& uri, const Atom& value) const +{ + return _properties.contains(uri, value); +} + +bool +Resource::has_property(const URI& uri, const URIs::Quark& value) const +{ + Properties::const_iterator i = _properties.find(uri); + for (; (i != _properties.end()) && (i->first == uri); ++i) { + if (value == i->second) { + return true; + } + } + return false; +} + +const Atom& +Resource::set_property(const URI& uri, const Atom& value) const +{ + return const_cast<Resource*>(this)->set_property(uri, value); +} + +const Atom& +Resource::get_property(const URI& uri) const +{ + static const Atom nil; + Properties::const_iterator i = _properties.find(uri); + return (i != _properties.end()) ? i->second : nil; +} + +bool +Resource::type(const URIs& uris, + const Properties& properties, + bool& graph, + bool& block, + bool& port, + bool& is_output) +{ + typedef Properties::const_iterator iterator; + const std::pair<iterator, iterator> types_range = properties.equal_range(uris.rdf_type); + + graph = block = port = is_output = false; + for (iterator i = types_range.first; i != types_range.second; ++i) { + const Atom& atom = i->second; + if (atom.type() != uris.forge.URI && atom.type() != uris.forge.URID) { + continue; // Non-URI type, ignore garbage data + } + + if (uris.ingen_Graph == atom) { + graph = true; + } else if (uris.ingen_Block == atom) { + block = true; + } else if (uris.lv2_InputPort == atom) { + port = true; + is_output = false; + } else if (uris.lv2_OutputPort == atom) { + port = true; + is_output = true; + } + } + + if (graph && block && !port) { // => graph + block = false; + return true; + } else if (port && (graph || block)) { // nonsense + port = false; + return false; + } else if (graph || block || port) { // recognized type + return true; + } else { // unknown + return false; + } +} + +void +Resource::set_properties(const Properties& props) +{ + /* Note a simple loop that calls set_property is inappropriate here since + it will not correctly set multiple properties in p (notably rdf:type) + */ + + // Erase existing properties with matching keys + for (const auto& p : props) { + _properties.erase(p.first); + on_property_removed(p.first, _uris.patch_wildcard.urid); + } + + // Set new properties + add_properties(props); +} + +void +Resource::add_properties(const Properties& props) +{ + for (const auto& p : props) { + add_property(p.first, p.second, p.second.context()); + } +} + +void +Resource::remove_properties(const Properties& props) +{ + for (const auto& p : props) { + remove_property(p.first, p.second); + } +} + +Properties +Resource::properties(Resource::Graph ctx) const +{ + Properties props; + for (const auto& p : _properties) { + if (p.second.context() == ctx) { + props.emplace(p.first, p.second); + } + } + + return props; +} + +} // namespace Ingen diff --git a/src/Serialiser.cpp b/src/Serialiser.cpp new file mode 100644 index 00000000..034ff96b --- /dev/null +++ b/src/Serialiser.cpp @@ -0,0 +1,583 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <set> +#include <string> +#include <utility> + +#include "ingen/Arc.hpp" +#include "ingen/FilePath.hpp" +#include "ingen/Forge.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Node.hpp" +#include "ingen/Resource.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/Store.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/filesystem.hpp" +#include "ingen/runtime_paths.hpp" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/extensions/ui/ui.h" +#include "raul/Path.hpp" +#include "sord/sordmm.hpp" +#include "sratom/sratom.h" + +namespace Ingen { + +struct Serialiser::Impl { + explicit Impl(World& world) + : _root_path("/") + , _world(world) + , _model(nullptr) + , _sratom(sratom_new(&_world.uri_map().urid_map_feature()->urid_map)) + {} + + ~Impl() { + sratom_free(_sratom); + } + + enum class Mode { TO_FILE, TO_STRING }; + + void start_to_file(const Raul::Path& root, + const FilePath& filename); + + std::set<const Resource*> serialise_graph(SPtr<const Node> graph, + const Sord::Node& graph_id); + + void serialise_block(SPtr<const Node> block, + const Sord::Node& class_id, + const Sord::Node& block_id); + + void serialise_port(const Node* port, + Resource::Graph context, + const Sord::Node& port_id); + + void serialise_properties(Sord::Node id, + const Properties& props); + + void write_bundle(SPtr<const Node> graph, const URI& uri); + + Sord::Node path_rdf_node(const Raul::Path& path); + + void write_manifest(const FilePath& bundle_path, + SPtr<const Node> graph); + + void write_plugins(const FilePath& bundle_path, + const std::set<const Resource*> plugins); + + void serialise_arc(const Sord::Node& parent, + SPtr<const Arc> arc); + + std::string finish(); + + Raul::Path _root_path; + Mode _mode; + URI _base_uri; + FilePath _basename; + World& _world; + Sord::Model* _model; + Sratom* _sratom; +}; + +Serialiser::Serialiser(World& world) + : me(new Impl(world)) +{} + +Serialiser::~Serialiser() +{ + delete me; +} + +void +Serialiser::Impl::write_manifest(const FilePath& bundle_path, + SPtr<const Node> graph) +{ + const FilePath manifest_path(bundle_path / "manifest.ttl"); + const FilePath binary_path(ingen_module_path("lv2")); + + start_to_file(Raul::Path("/"), manifest_path); + + Sord::World& world = _model->world(); + const URIs& uris = _world.uris(); + + const std::string filename("main.ttl"); + const Sord::URI subject(world, filename, _base_uri); + + _model->add_statement(subject, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.ingen_Graph)); + _model->add_statement(subject, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.lv2_Plugin)); + _model->add_statement(subject, + Sord::URI(world, uris.rdfs_seeAlso), + Sord::URI(world, filename, _base_uri)); + _model->add_statement(subject, + Sord::URI(world, uris.lv2_prototype), + Sord::URI(world, uris.ingen_GraphPrototype)); + + finish(); +} + +void +Serialiser::Impl::write_plugins(const FilePath& bundle_path, + const std::set<const Resource*> plugins) +{ + const FilePath plugins_path(bundle_path / "plugins.ttl"); + + start_to_file(Raul::Path("/"), plugins_path); + + Sord::World& world = _model->world(); + const URIs& uris = _world.uris(); + + for (const auto& p : plugins) { + const Atom& minor = p->get_property(uris.lv2_minorVersion); + const Atom& micro = p->get_property(uris.lv2_microVersion); + + _model->add_statement(Sord::URI(world, p->uri()), + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.lv2_Plugin)); + + if (minor.is_valid() && micro.is_valid()) { + _model->add_statement(Sord::URI(world, p->uri()), + Sord::URI(world, uris.lv2_minorVersion), + Sord::Literal::integer(world, minor.get<int32_t>())); + _model->add_statement(Sord::URI(world, p->uri()), + Sord::URI(world, uris.lv2_microVersion), + Sord::Literal::integer(world, micro.get<int32_t>())); + } + } + + finish(); +} + +void +Serialiser::write_bundle(SPtr<const Node> graph, const URI& uri) +{ + me->write_bundle(graph, uri); +} + +void +Serialiser::Impl::write_bundle(SPtr<const Node> graph, const URI& uri) +{ + FilePath path(uri.path()); + if (filesystem::exists(path) && !filesystem::is_directory(path)) { + path = path.parent_path(); + } + + _world.log().info(fmt("Writing bundle %1%\n") % path); + filesystem::create_directories(path); + + const FilePath main_file = path / "main.ttl"; + const Raul::Path old_root_path = _root_path; + + start_to_file(graph->path(), main_file); + + std::set<const Resource*> plugins = serialise_graph( + graph, + Sord::URI(_model->world(), main_file, _base_uri)); + + finish(); + write_manifest(path, graph); + write_plugins(path, plugins); + + _root_path = old_root_path; +} + +/** Begin a serialization to a file. + * + * This must be called before any serializing methods. + */ +void +Serialiser::Impl::start_to_file(const Raul::Path& root, + const FilePath& filename) +{ + _base_uri = URI(filename); + _basename = filename.stem(); + if (_basename == "main") { + _basename = filename.parent_path().stem(); + } + + _model = new Sord::Model(*_world.rdf_world(), _base_uri); + _mode = Mode::TO_FILE; + _root_path = root; +} + +void +Serialiser::start_to_string(const Raul::Path& root, const URI& base_uri) +{ + me->_root_path = root; + me->_base_uri = base_uri; + me->_model = new Sord::Model(*me->_world.rdf_world(), base_uri); + me->_mode = Impl::Mode::TO_STRING; +} + +void +Serialiser::start_to_file(const Raul::Path& root, const std::string& filename) +{ + me->start_to_file(root, filename); +} + +std::string +Serialiser::finish() +{ + return me->finish(); +} + +std::string +Serialiser::Impl::finish() +{ + std::string ret = ""; + if (_mode == Mode::TO_FILE) { + SerdStatus st = _model->write_to_file(_base_uri, SERD_TURTLE); + if (st) { + _world.log().error(fmt("Error writing file %1% (%2%)\n") + % _base_uri % serd_strerror(st)); + } + } else { + ret = _model->write_to_string(_base_uri, SERD_TURTLE); + } + + delete _model; + _model = nullptr; + _base_uri = URI(); + + return ret; +} + +Sord::Node +Serialiser::Impl::path_rdf_node(const Raul::Path& path) +{ + assert(_model); + assert(path == _root_path || path.is_child_of(_root_path)); + return Sord::URI(_model->world(), + path.substr(_root_path.base().length()), + _base_uri); +} + +void +Serialiser::serialise(SPtr<const Node> object) +{ + if (!me->_model) { + throw std::logic_error("serialise called without serialisation in progress"); + } + + if (object->graph_type() == Node::GraphType::GRAPH) { + me->serialise_graph(object, me->path_rdf_node(object->path())); + } else if (object->graph_type() == Node::GraphType::BLOCK) { + const Sord::URI plugin_id(me->_model->world(), object->plugin()->uri()); + me->serialise_block(object, plugin_id, me->path_rdf_node(object->path())); + } else if (object->graph_type() == Node::GraphType::PORT) { + me->serialise_port(object.get(), + Resource::Graph::DEFAULT, + me->path_rdf_node(object->path())); + } else { + me->serialise_properties(me->path_rdf_node(object->path()), + object->properties()); + } +} + +std::set<const Resource*> +Serialiser::Impl::serialise_graph(SPtr<const Node> graph, + const Sord::Node& graph_id) +{ + Sord::World& world = _model->world(); + const URIs& uris = _world.uris(); + + _model->add_statement(graph_id, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.ingen_Graph)); + + _model->add_statement(graph_id, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.lv2_Plugin)); + + _model->add_statement(graph_id, + Sord::URI(world, uris.lv2_extensionData), + Sord::URI(world, LV2_STATE__interface)); + + _model->add_statement(graph_id, + Sord::URI(world, LV2_UI__ui), + Sord::URI(world, "http://drobilla.net/ns/ingen#GraphUIGtk2")); + + // If the graph has no doap:name (required by LV2), use the basename + if (graph->properties().find(uris.doap_name) == graph->properties().end()) { + _model->add_statement(graph_id, + Sord::URI(world, uris.doap_name), + Sord::Literal(world, _basename)); + } + + const Properties props = graph->properties(Resource::Graph::INTERNAL); + serialise_properties(graph_id, props); + + std::set<const Resource*> plugins; + + const Store::const_range kids = _world.store()->children_range(graph); + for (Store::const_iterator n = kids.first; n != kids.second; ++n) { + if (n->first.parent() != graph->path()) { + continue; + } + + if (n->second->graph_type() == Node::GraphType::GRAPH) { + SPtr<Node> subgraph = n->second; + + SerdURI base_uri; + serd_uri_parse((const uint8_t*)_base_uri.c_str(), &base_uri); + + const std::string sub_bundle_path = subgraph->path().substr(1) + ".ingen"; + + SerdURI subgraph_uri; + SerdNode subgraph_node = serd_node_new_uri_from_string( + (const uint8_t*)sub_bundle_path.c_str(), + &base_uri, + &subgraph_uri); + + const Sord::URI subgraph_id(world, (const char*)subgraph_node.buf); + + // Save our state + URI my_base_uri = _base_uri; + Sord::Model* my_model = _model; + + // Write child bundle within this bundle + write_bundle(subgraph, subgraph_id); + + // Restore our state + _base_uri = my_base_uri; + _model = my_model; + + // Serialise reference to graph block + const Sord::Node block_id(path_rdf_node(subgraph->path())); + _model->add_statement(graph_id, + Sord::URI(world, uris.ingen_block), + block_id); + serialise_block(subgraph, subgraph_id, block_id); + } else if (n->second->graph_type() == Node::GraphType::BLOCK) { + SPtr<const Node> block = n->second; + + const Sord::URI class_id(world, block->plugin()->uri()); + const Sord::Node block_id(path_rdf_node(n->second->path())); + _model->add_statement(graph_id, + Sord::URI(world, uris.ingen_block), + block_id); + serialise_block(block, class_id, block_id); + + plugins.insert(block->plugin()); + } + } + + for (uint32_t i = 0; i < graph->num_ports(); ++i) { + Node* p = graph->port(i); + const Sord::Node port_id = path_rdf_node(p->path()); + + // Ensure lv2:name always exists so Graph is a valid LV2 plugin + if (p->properties().find(uris.lv2_name) == p->properties().end()) { + p->set_property(uris.lv2_name, + _world.forge().alloc(p->symbol().c_str())); + } + + _model->add_statement(graph_id, + Sord::URI(world, LV2_CORE__port), + port_id); + serialise_port(p, Resource::Graph::DEFAULT, port_id); + serialise_port(p, Resource::Graph::INTERNAL, port_id); + } + + for (const auto& a : graph->arcs()) { + serialise_arc(graph_id, a.second); + } + + return plugins; +} + +void +Serialiser::Impl::serialise_block(SPtr<const Node> block, + const Sord::Node& class_id, + const Sord::Node& block_id) +{ + const URIs& uris = _world.uris(); + + _model->add_statement(block_id, + Sord::URI(_model->world(), uris.rdf_type), + Sord::URI(_model->world(), uris.ingen_Block)); + _model->add_statement(block_id, + Sord::URI(_model->world(), uris.lv2_prototype), + class_id); + + // Serialise properties, but remove possibly stale state:state (set again below) + Properties props = block->properties(); + props.erase(uris.state_state); + serialise_properties(block_id, props); + + if (_base_uri.scheme() == "file") { + const FilePath base_path = _base_uri.file_path(); + const FilePath graph_dir = base_path.parent_path(); + const FilePath state_dir = graph_dir / block->symbol(); + const FilePath state_file = state_dir / "state.ttl"; + if (block->save_state(state_dir)) { + _model->add_statement(block_id, + Sord::URI(_model->world(), uris.state_state), + Sord::URI(_model->world(), URI(state_file))); + } + } + + for (uint32_t i = 0; i < block->num_ports(); ++i) { + Node* const p = block->port(i); + const Sord::Node port_id = path_rdf_node(p->path()); + serialise_port(p, Resource::Graph::DEFAULT, port_id); + _model->add_statement(block_id, + Sord::URI(_model->world(), uris.lv2_port), + port_id); + } +} + +void +Serialiser::Impl::serialise_port(const Node* port, + Resource::Graph context, + const Sord::Node& port_id) +{ + URIs& uris = _world.uris(); + Sord::World& world = _model->world(); + Properties props = port->properties(context); + + if (context == Resource::Graph::INTERNAL) { + // Always write lv2:symbol for Graph ports (required for lv2:Plugin) + _model->add_statement(port_id, + Sord::URI(world, uris.lv2_symbol), + Sord::Literal(world, port->path().symbol())); + } else if (context == Resource::Graph::EXTERNAL) { + // Never write lv2:index for plugin instances (not persistent/stable) + props.erase(uris.lv2_index); + } + + if (context == Resource::Graph::INTERNAL && + port->has_property(uris.rdf_type, uris.lv2_ControlPort) && + port->has_property(uris.rdf_type, uris.lv2_InputPort)) + { + const Atom& val = port->get_property(uris.ingen_value); + if (val.is_valid()) { + props.erase(uris.lv2_default); + props.emplace(uris.lv2_default, val); + } else { + _world.log().warn("Control input has no value, lv2:default omitted.\n"); + } + } else if (context != Resource::Graph::INTERNAL && + !port->has_property(uris.rdf_type, uris.lv2_InputPort)) { + props.erase(uris.ingen_value); + } + + serialise_properties(port_id, props); +} + +void +Serialiser::serialise_arc(const Sord::Node& parent, + SPtr<const Arc> arc) +{ + return me->serialise_arc(parent, arc); +} + +void +Serialiser::Impl::serialise_arc(const Sord::Node& parent, + SPtr<const Arc> arc) +{ + if (!_model) { + throw std::logic_error( + "serialise_arc called without serialisation in progress"); + } + + Sord::World& world = _model->world(); + const URIs& uris = _world.uris(); + + const Sord::Node src = path_rdf_node(arc->tail_path()); + const Sord::Node dst = path_rdf_node(arc->head_path()); + const Sord::Node arc_id = Sord::Node::blank_id(*_world.rdf_world()); + _model->add_statement(arc_id, + Sord::URI(world, uris.ingen_tail), + src); + _model->add_statement(arc_id, + Sord::URI(world, uris.ingen_head), + dst); + + if (parent.is_valid()) { + _model->add_statement(parent, + Sord::URI(world, uris.ingen_arc), + arc_id); + } else { + _model->add_statement(arc_id, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.ingen_Arc)); + } +} + +static bool +skip_property(Ingen::URIs& uris, const Sord::Node& predicate) +{ + return (predicate == INGEN__file || + predicate == uris.ingen_arc || + predicate == uris.ingen_block || + predicate == uris.lv2_port); +} + +void +Serialiser::Impl::serialise_properties(Sord::Node id, + const Properties& props) +{ + LV2_URID_Unmap* unmap = &_world.uri_map().urid_unmap_feature()->urid_unmap; + SerdNode base = serd_node_from_string(SERD_URI, + (const uint8_t*)_base_uri.c_str()); + SerdEnv* env = serd_env_new(&base); + SordInserter* inserter = sord_inserter_new(_model->c_obj(), env); + + sratom_set_sink(_sratom, _base_uri.c_str(), + (SerdStatementSink)sord_inserter_write_statement, nullptr, + inserter); + + sratom_set_pretty_numbers(_sratom, true); + + for (const auto& p : props) { + const Sord::URI key(_model->world(), p.first); + if (!skip_property(_world.uris(), key)) { + if (p.second.type() == _world.uris().atom_URI && + !strncmp((const char*)p.second.get_body(), "ingen:/main/", 13)) { + /* Value is a graph URI relative to the running engine. + Chop the prefix and save the path relative to the graph file. + This allows saving references to bundle resources. */ + sratom_write(_sratom, unmap, 0, + sord_node_to_serd_node(id.c_obj()), + sord_node_to_serd_node(key.c_obj()), + p.second.type(), p.second.size(), + (const char*)p.second.get_body() + 13); + } else { + sratom_write(_sratom, unmap, 0, + sord_node_to_serd_node(id.c_obj()), + sord_node_to_serd_node(key.c_obj()), + p.second.type(), p.second.size(), p.second.get_body()); + } + } + } + + sord_inserter_free(inserter); + serd_env_free(env); +} + +} // namespace Ingen diff --git a/src/SocketReader.cpp b/src/SocketReader.cpp new file mode 100644 index 00000000..13e95430 --- /dev/null +++ b/src/SocketReader.cpp @@ -0,0 +1,199 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cerrno> + +#include <poll.h> + +#include "ingen/AtomForgeSink.hpp" +#include "ingen/AtomReader.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/SocketReader.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "raul/Socket.hpp" +#include "sord/sordmm.hpp" +#include "sratom/sratom.h" + +namespace Ingen { + +SocketReader::SocketReader(Ingen::World& world, + Interface& iface, + SPtr<Raul::Socket> sock) + : _world(world) + , _iface(iface) + , _inserter(nullptr) + , _msg_node(nullptr) + , _socket(std::move(sock)) + , _exit_flag(false) + , _thread(&SocketReader::run, this) +{} + +SocketReader::~SocketReader() +{ + _exit_flag = true; + _socket->shutdown(); + _thread.join(); +} + +SerdStatus +SocketReader::set_base_uri(SocketReader* iface, + const SerdNode* uri_node) +{ + return sord_inserter_set_base_uri(iface->_inserter, uri_node); +} + +SerdStatus +SocketReader::set_prefix(SocketReader* iface, + const SerdNode* name, + const SerdNode* uri_node) +{ + return sord_inserter_set_prefix(iface->_inserter, name, uri_node); +} + +SerdStatus +SocketReader::write_statement(SocketReader* iface, + SerdStatementFlags flags, + const SerdNode* graph, + const SerdNode* subject, + const SerdNode* predicate, + const SerdNode* object, + const SerdNode* object_datatype, + const SerdNode* object_lang) +{ + if (!iface->_msg_node) { + iface->_msg_node = sord_node_from_serd_node( + iface->_world.rdf_world()->c_obj(), iface->_env, subject, nullptr, nullptr); + } + + return sord_inserter_write_statement( + iface->_inserter, flags, graph, + subject, predicate, object, + object_datatype, object_lang); +} + +void +SocketReader::run() +{ + Sord::World* world = _world.rdf_world(); + LV2_URID_Map* map = &_world.uri_map().urid_map_feature()->urid_map; + + // Open socket as a FILE for reading directly with serd + FILE* f = fdopen(_socket->fd(), "r"); + if (!f) { + _world.log().error(fmt("Failed to open connection (%1%)\n") + % strerror(errno)); + // Connection gone, exit + _socket.reset(); + return; + } + + // Set up sratom and a forge to build LV2 atoms from model + Sratom* sratom = sratom_new(map); + LV2_Atom_Forge forge; + lv2_atom_forge_init(&forge, map); + + AtomForgeSink buffer(&forge); + + SordNode* base_uri = nullptr; + SordModel* model = nullptr; + { + // Lock RDF world + std::lock_guard<std::mutex> lock(_world.rdf_mutex()); + + // Use <ingen:/> as base URI, so relative URIs are like bundle paths + base_uri = sord_new_uri(world->c_obj(), (const uint8_t*)"ingen:/"); + + // Make a model and reader to parse the next Turtle message + _env = world->prefixes().c_obj(); + model = sord_new(world->c_obj(), SORD_SPO, false); + + // Create an inserter for writing incoming triples to model + _inserter = sord_inserter_new(model, _env); + } + + SerdReader* reader = serd_reader_new( + SERD_TURTLE, this, nullptr, + (SerdBaseSink)set_base_uri, + (SerdPrefixSink)set_prefix, + (SerdStatementSink)write_statement, + nullptr); + + serd_env_set_base_uri(_env, sord_node_to_serd_node(base_uri)); + serd_reader_start_stream(reader, f, (const uint8_t*)"(socket)", false); + + // Make an AtomReader to call Ingen Interface methods based on Atom + AtomReader ar(_world.uri_map(), _world.uris(), _world.log(), _iface); + + struct pollfd pfd; + pfd.fd = _socket->fd(); + pfd.events = POLLIN; + pfd.revents = 0; + + while (!_exit_flag) { + if (feof(f)) { + break; // Lost connection + } + + // Wait for input to arrive at socket + int ret = poll(&pfd, 1, -1); + if (ret == -1 || (pfd.revents & (POLLERR|POLLHUP|POLLNVAL))) { + on_hangup(); + break; // Hangup + } else if (!ret) { + continue; // No data, shouldn't happen + } + + // Lock RDF world + std::lock_guard<std::mutex> lock(_world.rdf_mutex()); + + // Read until the next '.' + SerdStatus st = serd_reader_read_chunk(reader); + if (st == SERD_FAILURE || !_msg_node) { + continue; // Read nothing, e.g. just whitespace + } else if (st) { + _world.log().error(fmt("Read error: %1%\n") + % serd_strerror(st)); + continue; + } + + // Build an LV2_Atom at chunk.buf from the message + sratom_read(sratom, &forge, world->c_obj(), model, _msg_node); + + // Call _iface methods based on atom content + ar.write(buffer.atom()); + + // Reset everything for the next iteration + buffer.clear(); + sord_node_free(world->c_obj(), _msg_node); + _msg_node = nullptr; + } + + // Lock RDF world + std::lock_guard<std::mutex> lock(_world.rdf_mutex()); + + // Destroy everything + fclose(f); + sord_inserter_free(_inserter); + serd_reader_end_stream(reader); + sratom_free(sratom); + serd_reader_free(reader); + sord_free(model); + _socket.reset(); +} + +} // namespace Ingen diff --git a/src/SocketWriter.cpp b/src/SocketWriter.cpp new file mode 100644 index 00000000..68091bcc --- /dev/null +++ b/src/SocketWriter.cpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2012-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> + +#include "ingen/SocketWriter.hpp" +#include "raul/Socket.hpp" + +#ifndef MSG_NOSIGNAL +# define MSG_NOSIGNAL 0 +#endif + +namespace Ingen { + +SocketWriter::SocketWriter(URIMap& map, + URIs& uris, + const URI& uri, + SPtr<Raul::Socket> sock) + : TurtleWriter(map, uris, uri) + , _socket(std::move(sock)) +{} + +size_t +SocketWriter::text_sink(const void* buf, size_t len) +{ + ssize_t ret = send(_socket->fd(), buf, len, MSG_NOSIGNAL); + if (ret < 0) { + return 0; + } + return ret; +} + +void +SocketWriter::bundle_end() +{ + TurtleWriter::bundle_end(); + + // Send a NULL byte to indicate end of bundle + const char end[] = { 0 }; + send(_socket->fd(), end, 1, MSG_NOSIGNAL); +} + +} // namespace Ingen diff --git a/src/Store.cpp b/src/Store.cpp new file mode 100644 index 00000000..327ce416 --- /dev/null +++ b/src/Store.cpp @@ -0,0 +1,143 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <sstream> + +#include "ingen/Node.hpp" +#include "ingen/Store.hpp" + +namespace Ingen { + +void +Store::add(Node* o) +{ + if (find(o->path()) != end()) { + return; + } + + emplace(o->path(), SPtr<Node>(o)); + + for (uint32_t i = 0; i < o->num_ports(); ++i) { + add(o->port(i)); + } +} + +/* + TODO: These methods are currently O(n_children) but should logarithmic. The + std::map methods do not allow passing a comparator, but std::upper_bound + does. This should be achievable by making a rooted comparator that is a + normal ordering except compares a special sentinel value as the greatest + element that is a child of the parent. Searching for this sentinel should + then find the end of the descendants in logarithmic time. +*/ + +Store::iterator +Store::find_descendants_end(const iterator parent) +{ + iterator descendants_end = parent; + ++descendants_end; + while (descendants_end != end() && + descendants_end->first.is_child_of(parent->first)) { + ++descendants_end; + } + + return descendants_end; +} + +Store::const_iterator +Store::find_descendants_end(const const_iterator parent) const +{ + const_iterator descendants_end = parent; + ++descendants_end; + while (descendants_end != end() && + descendants_end->first.is_child_of(parent->first)) { + ++descendants_end; + } + + return descendants_end; +} + +Store::const_range +Store::children_range(SPtr<const Node> o) const +{ + const const_iterator parent = find(o->path()); + if (parent != end()) { + const_iterator first_child = parent; + ++first_child; + return std::make_pair(first_child, find_descendants_end(parent)); + } + return make_pair(end(), end()); +} + +void +Store::remove(const iterator top, Objects& removed) +{ + if (top != end()) { + const iterator descendants_end = find_descendants_end(top); + removed.insert(top, descendants_end); + erase(top, descendants_end); + } +} + +void +Store::rename(const iterator top, const Raul::Path& new_path) +{ + const Raul::Path old_path = top->first; + + // Remove the object and all its descendants + Objects removed; + remove(top, removed); + + // Rename all the removed objects + for (Objects::const_iterator i = removed.begin(); i != removed.end(); ++i) { + const Raul::Path path = (i->first == old_path) + ? new_path + : new_path.child( + Raul::Path(i->first.substr(old_path.base().length() - 1))); + + i->second->set_path(path); + assert(find(path) == end()); // Shouldn't be dropping objects! + emplace(path, i->second); + } +} + +unsigned +Store::child_name_offset(const Raul::Path& parent, + const Raul::Symbol& symbol, + bool allow_zero) const +{ + unsigned offset = 0; + + while (true) { + std::stringstream ss; + ss << symbol; + if (offset > 0) { + ss << "_" << offset; + } + if (find(parent.child(Raul::Symbol(ss.str()))) == end() && + (allow_zero || offset > 0)) { + break; + } else if (offset == 0) { + offset = 2; + } else { + ++offset; + } + } + + return offset; +} + +} // namespace Ingen diff --git a/src/StreamWriter.cpp b/src/StreamWriter.cpp new file mode 100644 index 00000000..45853055 --- /dev/null +++ b/src/StreamWriter.cpp @@ -0,0 +1,39 @@ +/* + This file is part of Ingen. + Copyright 2012-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/ColorContext.hpp" +#include "ingen/StreamWriter.hpp" + +namespace Ingen { + +StreamWriter::StreamWriter(URIMap& map, + URIs& uris, + const URI& uri, + FILE* stream, + ColorContext::Color color) + : TurtleWriter(map, uris, uri) + , _stream(stream) + , _color(color) +{} + +size_t +StreamWriter::text_sink(const void* buf, size_t len) +{ + ColorContext ctx(_stream, _color); + return fwrite(buf, 1, len, _stream); +} + +} // namespace Ingen diff --git a/src/TurtleWriter.cpp b/src/TurtleWriter.cpp new file mode 100644 index 00000000..368184d4 --- /dev/null +++ b/src/TurtleWriter.cpp @@ -0,0 +1,101 @@ +/* + This file is part of Ingen. + Copyright 2012-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/TurtleWriter.hpp" +#include "ingen/URIMap.hpp" + +#define USTR(s) ((const uint8_t*)(s)) + +namespace Ingen { + +static size_t +c_text_sink(const void* buf, size_t len, void* stream) +{ + TurtleWriter* writer = (TurtleWriter*)stream; + return writer->text_sink(buf, len); +} + +static SerdStatus +write_prefix(void* handle, const SerdNode* name, const SerdNode* uri) +{ + serd_writer_set_prefix((SerdWriter*)handle, name, uri); + return SERD_SUCCESS; +} + +TurtleWriter::TurtleWriter(URIMap& map, URIs& uris, const URI& uri) + : AtomWriter(map, uris, *this) + , _map(map) + , _sratom(sratom_new(&map.urid_map_feature()->urid_map)) + , _uri(uri) + , _wrote_prefixes(false) +{ + // Use <ingen:/> as base URI, so relative URIs are like bundle paths + _base = serd_node_from_string(SERD_URI, (const uint8_t*)"ingen:/"); + serd_uri_parse(_base.buf, &_base_uri); + + // Set up serialisation environment + _env = serd_env_new(&_base); + serd_env_set_prefix_from_strings(_env, USTR("atom"), USTR("http://lv2plug.in/ns/ext/atom#")); + serd_env_set_prefix_from_strings(_env, USTR("doap"), USTR("http://usefulinc.com/ns/doap#")); + serd_env_set_prefix_from_strings(_env, USTR("ingen"), USTR(INGEN_NS)); + serd_env_set_prefix_from_strings(_env, USTR("lv2"), USTR("http://lv2plug.in/ns/lv2core#")); + serd_env_set_prefix_from_strings(_env, USTR("midi"), USTR("http://lv2plug.in/ns/ext/midi#")); + serd_env_set_prefix_from_strings(_env, USTR("owl"), USTR("http://www.w3.org/2002/07/owl#")); + serd_env_set_prefix_from_strings(_env, USTR("patch"), USTR("http://lv2plug.in/ns/ext/patch#")); + serd_env_set_prefix_from_strings(_env, USTR("rdf"), USTR("http://www.w3.org/1999/02/22-rdf-syntax-ns#")); + serd_env_set_prefix_from_strings(_env, USTR("rdfs"), USTR("http://www.w3.org/2000/01/rdf-schema#")); + serd_env_set_prefix_from_strings(_env, USTR("xsd"), USTR("http://www.w3.org/2001/XMLSchema#")); + + // Make a Turtle writer that writes to text_sink + _writer = serd_writer_new( + SERD_TURTLE, + (SerdStyle)(SERD_STYLE_RESOLVED|SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED), + _env, + &_base_uri, + c_text_sink, + this); + + // Configure sratom to write directly to the writer (and thus text_sink) + sratom_set_sink(_sratom, + (const char*)_base.buf, + (SerdStatementSink)serd_writer_write_statement, + (SerdEndSink)serd_writer_end_anon, + _writer); +} + +TurtleWriter::~TurtleWriter() +{ + sratom_free(_sratom); + serd_writer_free(_writer); + serd_env_free(_env); +} + +bool +TurtleWriter::write(const LV2_Atom* msg, int32_t default_id) +{ + if (!_wrote_prefixes) { + // Write namespace prefixes once to reduce traffic + serd_env_foreach(_env, write_prefix, _writer); + _wrote_prefixes = true; + } + + sratom_write(_sratom, &_map.urid_unmap_feature()->urid_unmap, 0, + nullptr, nullptr, msg->type, msg->size, LV2_ATOM_BODY_CONST(msg)); + serd_writer_finish(_writer); + return true; +} + +} // namespace Ingen diff --git a/src/URI.cpp b/src/URI.cpp new file mode 100644 index 00000000..3e2d2a29 --- /dev/null +++ b/src/URI.cpp @@ -0,0 +1,113 @@ +/* + This file is part of Ingen. + Copyright 2018 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> + +#include "ingen/FilePath.hpp" +#include "ingen/URI.hpp" + +namespace Ingen { + +URI::URI() + : _node(SERD_NODE_NULL) + , _uri(SERD_URI_NULL) +{} + +URI::URI(const std::string& str) + : _node(serd_node_new_uri_from_string((const uint8_t*)str.c_str(), + NULL, + &_uri)) +{} + +URI::URI(const char* str) + : _node(serd_node_new_uri_from_string((const uint8_t*)str, NULL, &_uri)) +{} + +URI::URI(const std::string& str, const URI& base) + : _node(serd_node_new_uri_from_string((const uint8_t*)str.c_str(), + &base._uri, + &_uri)) +{} + +URI::URI(SerdNode node) + : _node(serd_node_new_uri_from_node(&node, NULL, &_uri)) +{ + assert(node.type == SERD_URI); +} + +URI::URI(SerdNode node, SerdURI uri) + : _node(node) + , _uri(uri) +{ + assert(node.type == SERD_URI); +} + +URI::URI(const Sord::Node& node) + : URI(*node.to_serd_node()) +{ +} + +URI::URI(const FilePath& path) + : _node(serd_node_new_file_uri((const uint8_t*)path.c_str(), + NULL, + &_uri, + true)) +{} + +URI::URI(const URI& uri) + : _node(serd_node_new_uri(&uri._uri, NULL, &_uri)) +{} + +URI& +URI::operator=(const URI& uri) +{ + serd_node_free(&_node); + _node = serd_node_new_uri(&uri._uri, NULL, &_uri); + return *this; +} + +URI::URI(URI&& uri) + : _node(uri._node) + , _uri(uri._uri) +{ + uri._node = SERD_NODE_NULL; + uri._uri = SERD_URI_NULL; +} + +URI& +URI::operator=(URI&& uri) +{ + _node = uri._node; + _uri = uri._uri; + uri._node = SERD_NODE_NULL; + uri._uri = SERD_URI_NULL; + return *this; +} + +URI::~URI() +{ + serd_node_free(&_node); +} + +URI +URI::make_relative(const URI& base) const +{ + SerdURI uri; + SerdNode node = serd_node_new_relative_uri(&_uri, &base._uri, NULL, &uri); + return URI(node, uri); +} + +} // namespace Ingen diff --git a/src/URIMap.cpp b/src/URIMap.cpp new file mode 100644 index 00000000..9ce1f178 --- /dev/null +++ b/src/URIMap.cpp @@ -0,0 +1,123 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdint> + +#include "ingen/Log.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" + +namespace Ingen { + +URIMap::URIMap(Log& log, LV2_URID_Map* map, LV2_URID_Unmap* unmap) + : _urid_map_feature(new URIDMapFeature(this, map, log)) + , _urid_unmap_feature(new URIDUnmapFeature(this, unmap)) +{ +} + +URIMap::URIDMapFeature::URIDMapFeature(URIMap* map, + LV2_URID_Map* impl, + Log& log) + : Feature(LV2_URID__map, &urid_map) + , log(log) +{ + if (impl) { + urid_map = *impl; + } else { + urid_map.map = default_map; + urid_map.handle = map; + } +} + +LV2_URID +URIMap::URIDMapFeature::default_map(LV2_URID_Map_Handle h, + const char* c_uri) +{ + URIMap* const map((URIMap*)h); + std::string uri(c_uri); + std::lock_guard<std::mutex> lock(map->_mutex); + + auto record = map->_map.emplace(uri, map->_map.size() + 1); + const auto id = record.first->second; + if (record.second) { + assert(id == map->_map.size()); + assert(id == map->_unmap.size() + 1); + map->_unmap.emplace_back(std::move(uri)); + } + return id; +} + +LV2_URID +URIMap::URIDMapFeature::map(const char* uri) +{ + if (!URI::is_valid(uri)) { + log.error(fmt("Attempt to map invalid URI <%1%>\n") % uri); + return 0; + } + return urid_map.map(urid_map.handle, uri); +} + +URIMap::URIDUnmapFeature::URIDUnmapFeature(URIMap* map, + LV2_URID_Unmap* impl) + : Feature(LV2_URID__unmap, &urid_unmap) +{ + if (impl) { + urid_unmap = *impl; + } else { + urid_unmap.unmap = default_unmap; + urid_unmap.handle = map; + } +} + +const char* +URIMap::URIDUnmapFeature::default_unmap(LV2_URID_Unmap_Handle h, + LV2_URID urid) +{ + URIMap* const map((URIMap*)h); + std::lock_guard<std::mutex> lock(map->_mutex); + + return (urid > 0 && urid <= map->_unmap.size() + ? map->_unmap[urid - 1].c_str() + : NULL); +} + +const char* +URIMap::URIDUnmapFeature::unmap(LV2_URID urid) +{ + return urid_unmap.unmap(urid_unmap.handle, urid); +} + +uint32_t +URIMap::map_uri(const char* uri) +{ + const uint32_t urid = _urid_map_feature->map(uri); +#ifdef INGEN_DEBUG_URIDS + fprintf(stderr, "Map URI %3u <= %s\n", urid, uri); +#endif + return urid; +} + +const char* +URIMap::unmap_uri(uint32_t urid) const +{ + const char* uri = _urid_unmap_feature->unmap(urid); +#ifdef INGEN_DEBUG_URIDS + fprintf(stderr, "Unmap URI %3u => %s\n", urid, uri); +#endif + return uri; +} + +} // namespace Ingen diff --git a/src/URIs.cpp b/src/URIs.cpp new file mode 100644 index 00000000..af03b7b5 --- /dev/null +++ b/src/URIs.cpp @@ -0,0 +1,204 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/morph/morph.h" +#include "lv2/lv2plug.in/ns/ext/options/options.h" +#include "lv2/lv2plug.in/ns/ext/parameters/parameters.h" +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "lv2/lv2plug.in/ns/ext/port-props/port-props.h" +#include "lv2/lv2plug.in/ns/ext/presets/presets.h" +#include "lv2/lv2plug.in/ns/ext/resize-port/resize-port.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/ext/time/time.h" +#include "lv2/lv2plug.in/ns/ext/worker/worker.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +namespace Ingen { + +URIs::Quark::Quark(Forge& forge, + URIMap* map, + LilvWorld* lworld, + const char* str) + : URI(str) + , urid(forge.make_urid(URI(str))) + , uri(forge.alloc_uri(str)) + , lnode(lilv_new_uri(lworld, str)) +{} + +URIs::Quark::Quark(const Quark& copy) + : URI(copy) + , urid(copy.urid) + , uri(copy.uri) + , lnode(lilv_node_duplicate(copy.lnode)) +{} + +URIs::Quark::~Quark() +{ + lilv_node_free(lnode); +} + +#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define NS_RDFS "http://www.w3.org/2000/01/rdf-schema#" + +URIs::URIs(Forge& forge, URIMap* map, LilvWorld* lworld) + : forge(forge) + , atom_AtomPort (forge, map, lworld, LV2_ATOM__AtomPort) + , atom_Bool (forge, map, lworld, LV2_ATOM__Bool) + , atom_Chunk (forge, map, lworld, LV2_ATOM__Chunk) + , atom_Float (forge, map, lworld, LV2_ATOM__Float) + , atom_Int (forge, map, lworld, LV2_ATOM__Int) + , atom_Object (forge, map, lworld, LV2_ATOM__Object) + , atom_Path (forge, map, lworld, LV2_ATOM__Path) + , atom_Sequence (forge, map, lworld, LV2_ATOM__Sequence) + , atom_Sound (forge, map, lworld, LV2_ATOM__Sound) + , atom_String (forge, map, lworld, LV2_ATOM__String) + , atom_URI (forge, map, lworld, LV2_ATOM__URI) + , atom_URID (forge, map, lworld, LV2_ATOM__URID) + , atom_bufferType (forge, map, lworld, LV2_ATOM__bufferType) + , atom_eventTransfer (forge, map, lworld, LV2_ATOM__eventTransfer) + , atom_supports (forge, map, lworld, LV2_ATOM__supports) + , bufsz_maxBlockLength (forge, map, lworld, LV2_BUF_SIZE__maxBlockLength) + , bufsz_minBlockLength (forge, map, lworld, LV2_BUF_SIZE__minBlockLength) + , bufsz_sequenceSize (forge, map, lworld, LV2_BUF_SIZE__sequenceSize) + , doap_name (forge, map, lworld, "http://usefulinc.com/ns/doap#name") + , ingen_Arc (forge, map, lworld, INGEN__Arc) + , ingen_Block (forge, map, lworld, INGEN__Block) + , ingen_BundleEnd (forge, map, lworld, INGEN__BundleEnd) + , ingen_BundleStart (forge, map, lworld, INGEN__BundleStart) + , ingen_Graph (forge, map, lworld, INGEN__Graph) + , ingen_GraphPrototype (forge, map, lworld, INGEN__GraphPrototype) + , ingen_Internal (forge, map, lworld, INGEN__Internal) + , ingen_Redo (forge, map, lworld, INGEN__Redo) + , ingen_Undo (forge, map, lworld, INGEN__Undo) + , ingen_activity (forge, map, lworld, INGEN__activity) + , ingen_arc (forge, map, lworld, INGEN__arc) + , ingen_block (forge, map, lworld, INGEN__block) + , ingen_broadcast (forge, map, lworld, INGEN__broadcast) + , ingen_canvasX (forge, map, lworld, INGEN__canvasX) + , ingen_canvasY (forge, map, lworld, INGEN__canvasY) + , ingen_enabled (forge, map, lworld, INGEN__enabled) + , ingen_externalContext (forge, map, lworld, INGEN__externalContext) + , ingen_file (forge, map, lworld, INGEN__file) + , ingen_head (forge, map, lworld, INGEN__head) + , ingen_incidentTo (forge, map, lworld, INGEN__incidentTo) + , ingen_internalContext (forge, map, lworld, INGEN__internalContext) + , ingen_loadedBundle (forge, map, lworld, INGEN__loadedBundle) + , ingen_maxRunLoad (forge, map, lworld, INGEN__maxRunLoad) + , ingen_meanRunLoad (forge, map, lworld, INGEN__meanRunLoad) + , ingen_minRunLoad (forge, map, lworld, INGEN__minRunLoad) + , ingen_numThreads (forge, map, lworld, INGEN__numThreads) + , ingen_polyphonic (forge, map, lworld, INGEN__polyphonic) + , ingen_polyphony (forge, map, lworld, INGEN__polyphony) + , ingen_prototype (forge, map, lworld, INGEN__prototype) + , ingen_sprungLayout (forge, map, lworld, INGEN__sprungLayout) + , ingen_tail (forge, map, lworld, INGEN__tail) + , ingen_uiEmbedded (forge, map, lworld, INGEN__uiEmbedded) + , ingen_value (forge, map, lworld, INGEN__value) + , log_Error (forge, map, lworld, LV2_LOG__Error) + , log_Note (forge, map, lworld, LV2_LOG__Note) + , log_Trace (forge, map, lworld, LV2_LOG__Trace) + , log_Warning (forge, map, lworld, LV2_LOG__Warning) + , lv2_AudioPort (forge, map, lworld, LV2_CORE__AudioPort) + , lv2_CVPort (forge, map, lworld, LV2_CORE__CVPort) + , lv2_ControlPort (forge, map, lworld, LV2_CORE__ControlPort) + , lv2_InputPort (forge, map, lworld, LV2_CORE__InputPort) + , lv2_OutputPort (forge, map, lworld, LV2_CORE__OutputPort) + , lv2_Plugin (forge, map, lworld, LV2_CORE__Plugin) + , lv2_appliesTo (forge, map, lworld, LV2_CORE__appliesTo) + , lv2_binary (forge, map, lworld, LV2_CORE__binary) + , lv2_connectionOptional(forge, map, lworld, LV2_CORE__connectionOptional) + , lv2_control (forge, map, lworld, LV2_CORE__control) + , lv2_default (forge, map, lworld, LV2_CORE__default) + , lv2_designation (forge, map, lworld, LV2_CORE__designation) + , lv2_enumeration (forge, map, lworld, LV2_CORE__enumeration) + , lv2_extensionData (forge, map, lworld, LV2_CORE__extensionData) + , lv2_index (forge, map, lworld, LV2_CORE__index) + , lv2_integer (forge, map, lworld, LV2_CORE__integer) + , lv2_maximum (forge, map, lworld, LV2_CORE__maximum) + , lv2_microVersion (forge, map, lworld, LV2_CORE__microVersion) + , lv2_minimum (forge, map, lworld, LV2_CORE__minimum) + , lv2_minorVersion (forge, map, lworld, LV2_CORE__minorVersion) + , lv2_name (forge, map, lworld, LV2_CORE__name) + , lv2_port (forge, map, lworld, LV2_CORE__port) + , lv2_portProperty (forge, map, lworld, LV2_CORE__portProperty) + , lv2_prototype (forge, map, lworld, LV2_CORE__prototype) + , lv2_sampleRate (forge, map, lworld, LV2_CORE__sampleRate) + , lv2_scalePoint (forge, map, lworld, LV2_CORE__scalePoint) + , lv2_symbol (forge, map, lworld, LV2_CORE__symbol) + , lv2_toggled (forge, map, lworld, LV2_CORE__toggled) + , midi_Bender (forge, map, lworld, LV2_MIDI__Bender) + , midi_ChannelPressure (forge, map, lworld, LV2_MIDI__ChannelPressure) + , midi_Controller (forge, map, lworld, LV2_MIDI__Controller) + , midi_MidiEvent (forge, map, lworld, LV2_MIDI__MidiEvent) + , midi_NoteOn (forge, map, lworld, LV2_MIDI__NoteOn) + , midi_binding (forge, map, lworld, LV2_MIDI__binding) + , midi_controllerNumber (forge, map, lworld, LV2_MIDI__controllerNumber) + , midi_noteNumber (forge, map, lworld, LV2_MIDI__noteNumber) + , morph_AutoMorphPort (forge, map, lworld, LV2_MORPH__AutoMorphPort) + , morph_MorphPort (forge, map, lworld, LV2_MORPH__MorphPort) + , morph_currentType (forge, map, lworld, LV2_MORPH__currentType) + , morph_supportsType (forge, map, lworld, LV2_MORPH__supportsType) + , opt_interface (forge, map, lworld, LV2_OPTIONS__interface) + , param_sampleRate (forge, map, lworld, LV2_PARAMETERS__sampleRate) + , patch_Copy (forge, map, lworld, LV2_PATCH__Copy) + , patch_Delete (forge, map, lworld, LV2_PATCH__Delete) + , patch_Get (forge, map, lworld, LV2_PATCH__Get) + , patch_Message (forge, map, lworld, LV2_PATCH__Message) + , patch_Move (forge, map, lworld, LV2_PATCH__Move) + , patch_Patch (forge, map, lworld, LV2_PATCH__Patch) + , patch_Put (forge, map, lworld, LV2_PATCH__Put) + , patch_Response (forge, map, lworld, LV2_PATCH__Response) + , patch_Set (forge, map, lworld, LV2_PATCH__Set) + , patch_add (forge, map, lworld, LV2_PATCH__add) + , patch_body (forge, map, lworld, LV2_PATCH__body) + , patch_context (forge, map, lworld, LV2_PATCH__context) + , patch_destination (forge, map, lworld, LV2_PATCH__destination) + , patch_property (forge, map, lworld, LV2_PATCH__property) + , patch_remove (forge, map, lworld, LV2_PATCH__remove) + , patch_sequenceNumber (forge, map, lworld, LV2_PATCH__sequenceNumber) + , patch_subject (forge, map, lworld, LV2_PATCH__subject) + , patch_value (forge, map, lworld, LV2_PATCH__value) + , patch_wildcard (forge, map, lworld, LV2_PATCH__wildcard) + , pprops_logarithmic (forge, map, lworld, LV2_PORT_PROPS__logarithmic) + , pset_Preset (forge, map, lworld, LV2_PRESETS__Preset) + , pset_preset (forge, map, lworld, LV2_PRESETS__preset) + , rdf_type (forge, map, lworld, NS_RDF "type") + , rdfs_Class (forge, map, lworld, NS_RDFS "Class") + , rdfs_label (forge, map, lworld, NS_RDFS "label") + , rdfs_seeAlso (forge, map, lworld, NS_RDFS "seeAlso") + , rsz_minimumSize (forge, map, lworld, LV2_RESIZE_PORT__minimumSize) + , state_loadDefaultState(forge, map, lworld, LV2_STATE__loadDefaultState) + , state_state (forge, map, lworld, LV2_STATE__state) + , time_Position (forge, map, lworld, LV2_TIME__Position) + , time_bar (forge, map, lworld, LV2_TIME__bar) + , time_barBeat (forge, map, lworld, LV2_TIME__barBeat) + , time_beatUnit (forge, map, lworld, LV2_TIME__beatUnit) + , time_beatsPerBar (forge, map, lworld, LV2_TIME__beatsPerBar) + , time_beatsPerMinute (forge, map, lworld, LV2_TIME__beatsPerMinute) + , time_frame (forge, map, lworld, LV2_TIME__frame) + , time_speed (forge, map, lworld, LV2_TIME__speed) + , work_schedule (forge, map, lworld, LV2_WORKER__schedule) +{} + +} // namespace Ingen diff --git a/src/World.cpp b/src/World.cpp new file mode 100644 index 00000000..568ab405 --- /dev/null +++ b/src/World.cpp @@ -0,0 +1,355 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <map> +#include <memory> +#include <string> +#include <utility> + +#include "ingen/Configuration.hpp" +#include "ingen/DataAccess.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Forge.hpp" +#include "ingen/InstanceAccess.hpp" +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/Parser.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/filesystem.hpp" +#include "ingen/ingen.h" +#include "ingen/runtime_paths.hpp" +#include "lilv/lilv.h" +#include "sord/sordmm.hpp" + +using std::string; + +namespace Ingen { + +class EngineBase; +class Interface; +class Parser; +class Serialiser; +class Store; + +/** Load a dynamic module from the default path. + * + * This will check in the directories specified in the environment variable + * INGEN_MODULE_PATH (typical colon delimited format), then the default module + * installation directory (ie /usr/local/lib/ingen), in that order. + * + * \param name The base name of the module, e.g. "ingen_jack" + */ +static std::unique_ptr<Library> +ingen_load_library(Log& log, const string& name) +{ + std::unique_ptr<Library> library; + + // Search INGEN_MODULE_PATH first + const char* const module_path = getenv("INGEN_MODULE_PATH"); + if (module_path) { + string dir; + std::istringstream iss(module_path); + while (getline(iss, dir, search_path_separator)) { + FilePath filename = Ingen::ingen_module_path(name, FilePath(dir)); + if (filesystem::exists(filename)) { + library = std::unique_ptr<Library>(new Library(filename)); + if (*library) { + return library; + } else { + log.error(Library::get_last_error()); + } + } + } + } + + // Try default directory if not found + library = std::unique_ptr<Library>(new Library(Ingen::ingen_module_path(name))); + + if (*library) { + return library; + } else if (!module_path) { + log.error(fmt("Unable to find %1% (%2%)\n") + % name % Library::get_last_error()); + return nullptr; + } else { + log.error(fmt("Unable to load %1% from %2% (%3%)\n") + % name % module_path % Library::get_last_error()); + return nullptr; + } +} + +class World::Impl { +public: + Impl(LV2_URID_Map* map, + LV2_URID_Unmap* unmap, + LV2_Log_Log* lv2_log) + : argc(nullptr) + , argv(nullptr) + , lv2_features(nullptr) + , rdf_world(new Sord::World()) + , lilv_world(lilv_world_new()) + , uri_map(new URIMap(log, map, unmap)) + , forge(new Forge(*uri_map)) + , uris(new URIs(*forge, uri_map, lilv_world)) + , conf(*forge) + , log(lv2_log, *uris) + { + lv2_features = new LV2Features(); + lv2_features->add_feature(uri_map->urid_map_feature()); + lv2_features->add_feature(uri_map->urid_unmap_feature()); + lv2_features->add_feature(SPtr<InstanceAccess>(new InstanceAccess())); + lv2_features->add_feature(SPtr<DataAccess>(new DataAccess())); + lv2_features->add_feature(SPtr<Log::Feature>(new Log::Feature())); + lilv_world_load_all(lilv_world); + + // Set up RDF namespaces + rdf_world->add_prefix("atom", "http://lv2plug.in/ns/ext/atom#"); + rdf_world->add_prefix("doap", "http://usefulinc.com/ns/doap#"); + rdf_world->add_prefix("ingen", INGEN_NS); + rdf_world->add_prefix("lv2", "http://lv2plug.in/ns/lv2core#"); + rdf_world->add_prefix("midi", "http://lv2plug.in/ns/ext/midi#"); + rdf_world->add_prefix("owl", "http://www.w3.org/2002/07/owl#"); + rdf_world->add_prefix("patch", "http://lv2plug.in/ns/ext/patch#"); + rdf_world->add_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); + rdf_world->add_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#"); + rdf_world->add_prefix("xsd", "http://www.w3.org/2001/XMLSchema#"); + + // Load internal 'plugin' information into lilv world + LilvNode* rdf_type = lilv_new_uri( + lilv_world, "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); + LilvNode* ingen_Plugin = lilv_new_uri( + lilv_world, INGEN__Plugin); + LilvNodes* internals = lilv_world_find_nodes( + lilv_world, nullptr, rdf_type, ingen_Plugin); + LILV_FOREACH(nodes, i, internals) { + const LilvNode* internal = lilv_nodes_get(internals, i); + lilv_world_load_resource(lilv_world, internal); + } + lilv_nodes_free(internals); + lilv_node_free(rdf_type); + lilv_node_free(ingen_Plugin); + } + + ~Impl() + { + if (engine) { + engine->quit(); + } + + // Delete module objects but save pointers to libraries + typedef std::list<std::unique_ptr<Library>> Libs; + Libs libs; + for (auto& m : modules) { + libs.emplace_back(std::move(m.second->library)); + delete m.second; + } + + serialiser.reset(); + parser.reset(); + interface.reset(); + engine.reset(); + store.reset(); + + interface_factories.clear(); + script_runners.clear(); + + delete rdf_world; + delete lv2_features; + delete uris; + delete forge; + delete uri_map; + + lilv_world_free(lilv_world); + + // Module libraries go out of scope and close here + } + + typedef std::map<const std::string, Module*> Modules; + Modules modules; + + typedef std::map<const std::string, World::InterfaceFactory> InterfaceFactories; + InterfaceFactories interface_factories; + + typedef bool (*ScriptRunner)(World* world, const char* filename); + typedef std::map<const std::string, ScriptRunner> ScriptRunners; + ScriptRunners script_runners; + + int* argc; + char*** argv; + LV2Features* lv2_features; + Sord::World* rdf_world; + LilvWorld* lilv_world; + URIMap* uri_map; + Forge* forge; + URIs* uris; + LV2_Log_Log* lv2_log; + Configuration conf; + Log log; + SPtr<Interface> interface; + SPtr<EngineBase> engine; + SPtr<Serialiser> serialiser; + SPtr<Parser> parser; + SPtr<Store> store; + std::mutex rdf_mutex; + std::string jack_uuid; +}; + +World::World(LV2_URID_Map* map, LV2_URID_Unmap* unmap, LV2_Log_Log* log) + : _impl(new Impl(map, unmap, log)) +{ + _impl->serialiser = SPtr<Serialiser>(new Serialiser(*this)); + _impl->parser = SPtr<Parser>(new Parser()); +} + +World::~World() +{ + delete _impl; +} + +void +World::load_configuration(int& argc, char**& argv) +{ + _impl->argc = &argc; + _impl->argv = &argv; + + // Parse default configuration files + const auto files = _impl->conf.load_default("ingen", "options.ttl"); + for (const auto& f : files) { + _impl->log.info(fmt("Loaded configuration %1%\n") % f); + } + + // Parse command line options, overriding configuration file values + _impl->conf.parse(argc, argv); + _impl->log.set_flush(_impl->conf.option("flush-log").get<int32_t>()); + _impl->log.set_trace(_impl->conf.option("trace").get<int32_t>()); +} + +void World::set_engine(SPtr<EngineBase> e) { _impl->engine = e; } +void World::set_interface(SPtr<Interface> i) { _impl->interface = i; } +void World::set_store(SPtr<Store> s) { _impl->store = s; } + +SPtr<EngineBase> World::engine() { return _impl->engine; } +SPtr<Interface> World::interface() { return _impl->interface; } +SPtr<Parser> World::parser() { return _impl->parser; } +SPtr<Serialiser> World::serialiser() { return _impl->serialiser; } +SPtr<Store> World::store() { return _impl->store; } + +int& World::argc() { return *_impl->argc; } +char**& World::argv() { return *_impl->argv; } +Configuration& World::conf() { return _impl->conf; } +Log& World::log() { return _impl->log; } + +std::mutex& World::rdf_mutex() { return _impl->rdf_mutex; } + +Sord::World* World::rdf_world() { return _impl->rdf_world; } +LilvWorld* World::lilv_world() { return _impl->lilv_world; } + +LV2Features& World::lv2_features() { return *_impl->lv2_features; } +Forge& World::forge() { return *_impl->forge; } +URIs& World::uris() { return *_impl->uris; } +URIMap& World::uri_map() { return *_impl->uri_map; } + +bool +World::load_module(const char* name) +{ + auto i = _impl->modules.find(name); + if (i != _impl->modules.end()) { + return true; + } + log().info(fmt("Loading %1% module\n") % name); + std::unique_ptr<Ingen::Library> lib = ingen_load_library(log(), name); + Ingen::Module* (*module_load)() = + lib ? (Ingen::Module* (*)())lib->get_function("ingen_module_load") + : nullptr; + if (module_load) { + Module* module = module_load(); + if (module) { + module->library = std::move(lib); + module->load(this); + _impl->modules.emplace(string(name), module); + return true; + } + } + + log().error(fmt("Failed to load module `%1%' (%2%)\n") % name % lib->get_last_error()); + return false; +} + +bool +World::run_module(const char* name) +{ + auto i = _impl->modules.find(name); + if (i == _impl->modules.end()) { + log().error(fmt("Attempt to run unloaded module `%1%'\n") % name); + return false; + } + + i->second->run(this); + return true; +} + +/** Get an interface for a remote engine at `engine_uri` + */ +SPtr<Interface> +World::new_interface(const URI& engine_uri, SPtr<Interface> respondee) +{ + const Impl::InterfaceFactories::const_iterator i = + _impl->interface_factories.find(std::string(engine_uri.scheme())); + if (i == _impl->interface_factories.end()) { + log().warn(fmt("Unknown URI scheme `%1%'\n") % engine_uri.scheme()); + return SPtr<Interface>(); + } + + return i->second(this, engine_uri, respondee); +} + +/** Run a script of type `mime_type` at filename `filename` */ +bool +World::run(const std::string& mime_type, const std::string& filename) +{ + const Impl::ScriptRunners::const_iterator i = _impl->script_runners.find(mime_type); + if (i == _impl->script_runners.end()) { + log().warn(fmt("Unknown script MIME type `%1%'\n") % mime_type); + return false; + } + + return i->second(this, filename.c_str()); +} + +void +World::add_interface_factory(const std::string& scheme, InterfaceFactory factory) +{ + _impl->interface_factories.emplace(scheme, factory); +} + +void +World::set_jack_uuid(const std::string& uuid) +{ + _impl->jack_uuid = uuid; +} + +std::string +World::jack_uuid() +{ + return _impl->jack_uuid; +} + +} // namespace Ingen diff --git a/src/client/BlockModel.cpp b/src/client/BlockModel.cpp new file mode 100644 index 00000000..910f7037 --- /dev/null +++ b/src/client/BlockModel.cpp @@ -0,0 +1,285 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cmath> +#include <string> + +#include "ingen/client/BlockModel.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" + +namespace Ingen { +namespace Client { + +BlockModel::BlockModel(URIs& uris, + SPtr<PluginModel> plugin, + const Raul::Path& path) + : ObjectModel(uris, path) + , _plugin_uri(plugin->uri()) + , _plugin(plugin) + , _num_values(0) + , _min_values(nullptr) + , _max_values(nullptr) +{ +} + +BlockModel::BlockModel(URIs& uris, + const URI& plugin_uri, + const Raul::Path& path) + : ObjectModel(uris, path) + , _plugin_uri(plugin_uri) + , _num_values(0) + , _min_values(nullptr) + , _max_values(nullptr) +{ +} + +BlockModel::BlockModel(const BlockModel& copy) + : ObjectModel(copy) + , _plugin_uri(copy._plugin_uri) + , _num_values(copy._num_values) + , _min_values((float*)malloc(sizeof(float) * _num_values)) + , _max_values((float*)malloc(sizeof(float) * _num_values)) +{ + memcpy(_min_values, copy._min_values, sizeof(float) * _num_values); + memcpy(_max_values, copy._max_values, sizeof(float) * _num_values); +} + +BlockModel::~BlockModel() +{ + clear(); +} + +void +BlockModel::remove_port(SPtr<PortModel> port) +{ + for (auto i = _ports.begin(); i != _ports.end(); ++i) { + if ((*i) == port) { + _ports.erase(i); + break; + } + } + _signal_removed_port.emit(port); +} + +void +BlockModel::remove_port(const Raul::Path& port_path) +{ + for (auto i = _ports.begin(); i != _ports.end(); ++i) { + if ((*i)->path() == port_path) { + _ports.erase(i); + break; + } + } +} + +void +BlockModel::clear() +{ + _ports.clear(); + assert(_ports.empty()); + delete[] _min_values; + delete[] _max_values; + _min_values = nullptr; + _max_values = nullptr; +} + +void +BlockModel::add_child(SPtr<ObjectModel> c) +{ + assert(c->parent().get() == this); + + //ObjectModel::add_child(c); + + SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(c); + assert(pm); + add_port(pm); +} + +bool +BlockModel::remove_child(SPtr<ObjectModel> c) +{ + assert(c->path().is_child_of(path())); + assert(c->parent().get() == this); + + //bool ret = ObjectModel::remove_child(c); + + SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(c); + assert(pm); + remove_port(pm); + + //return ret; + return true; +} + +void +BlockModel::add_port(SPtr<PortModel> pm) +{ + assert(pm); + assert(pm->path().is_child_of(path())); + assert(pm->parent().get() == this); + + // Store should have handled this by merging the two + assert(find(_ports.begin(), _ports.end(), pm) == _ports.end()); + + _ports.push_back(pm); + _signal_new_port.emit(pm); +} + +SPtr<const PortModel> +BlockModel::get_port(const Raul::Symbol& symbol) const +{ + for (auto p : _ports) { + if (p->symbol() == symbol) { + return p; + } + } + return SPtr<PortModel>(); +} + +SPtr<const PortModel> +BlockModel::get_port(uint32_t index) const +{ + return _ports[index]; +} + +Ingen::Node* +BlockModel::port(uint32_t index) const +{ + assert(index < num_ports()); + return const_cast<Ingen::Node*>( + dynamic_cast<const Ingen::Node*>(_ports[index].get())); +} + +void +BlockModel::default_port_value_range(SPtr<const PortModel> port, + float& min, + float& max, + uint32_t srate) const +{ + // Default control values + min = 0.0; + max = 1.0; + + // Get range from client-side LV2 data + if (_plugin && _plugin->lilv_plugin()) { + if (!_min_values) { + _num_values = lilv_plugin_get_num_ports(_plugin->lilv_plugin()); + _min_values = new float[_num_values]; + _max_values = new float[_num_values]; + lilv_plugin_get_port_ranges_float(_plugin->lilv_plugin(), + _min_values, _max_values, nullptr); + } + + if (!std::isnan(_min_values[port->index()])) { + min = _min_values[port->index()]; + } + if (!std::isnan(_max_values[port->index()])) { + max = _max_values[port->index()]; + } + } + + if (port->port_property(_uris.lv2_sampleRate)) { + min *= srate; + max *= srate; + } +} + +void +BlockModel::port_value_range(SPtr<const PortModel> port, + float& min, + float& max, + uint32_t srate) const +{ + assert(port->parent().get() == this); + + default_port_value_range(port, min, max); + + // Possibly overriden + const Atom& min_atom = port->get_property(_uris.lv2_minimum); + const Atom& max_atom = port->get_property(_uris.lv2_maximum); + if (min_atom.type() == _uris.forge.Float) { + min = min_atom.get<float>(); + } + if (max_atom.type() == _uris.forge.Float) { + max = max_atom.get<float>(); + } + + if (max <= min) { + max = min + 1.0; + } + + if (port->port_property(_uris.lv2_sampleRate)) { + min *= srate; + max *= srate; + } +} + +std::string +BlockModel::label() const +{ + const Atom& name_property = get_property(_uris.lv2_name); + if (name_property.type() == _uris.forge.String) { + return name_property.ptr<char>(); + } else if (plugin_model()) { + return plugin_model()->human_name(); + } else { + return symbol().c_str(); + } +} + +std::string +BlockModel::port_label(SPtr<const PortModel> port) const +{ + const Atom& name = port->get_property(URI(LV2_CORE__name)); + if (name.is_valid() && name.type() == _uris.forge.String) { + return name.ptr<char>(); + } + + if (_plugin && _plugin->lilv_plugin()) { + LilvWorld* w = _plugin->lilv_world(); + const LilvPlugin* plug = _plugin->lilv_plugin(); + LilvNode* sym = lilv_new_string(w, port->symbol().c_str()); + const LilvPort* lport = lilv_plugin_get_port_by_symbol(plug, sym); + if (lport) { + LilvNode* lname = lilv_port_get_name(plug, lport); + if (lname && lilv_node_is_string(lname)) { + std::string ret(lilv_node_as_string(lname)); + lilv_node_free(lname); + return ret; + } + lilv_node_free(lname); + } + } + + return port->symbol().c_str(); +} + +void +BlockModel::set(SPtr<ObjectModel> model) +{ + SPtr<BlockModel> block = dynamic_ptr_cast<BlockModel>(model); + if (block) { + _plugin_uri = block->_plugin_uri; + _plugin = block->_plugin; + } + + ObjectModel::set(model); +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/ClientStore.cpp b/src/client/ClientStore.cpp new file mode 100644 index 00000000..792f8949 --- /dev/null +++ b/src/client/ClientStore.cpp @@ -0,0 +1,487 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <boost/variant/apply_visitor.hpp> + +#include "ingen/Log.hpp" +#include "ingen/client/ArcModel.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/ObjectModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/client/PortModel.hpp" +#include "ingen/client/SigClientInterface.hpp" + +namespace Ingen { +namespace Client { + +ClientStore::ClientStore(URIs& uris, + Log& log, + SPtr<SigClientInterface> emitter) + : _uris(uris) + , _log(log) + , _emitter(emitter) + , _plugins(new Plugins()) +{ + if (emitter) { + emitter->signal_message().connect( + sigc::mem_fun(this, &ClientStore::message)); + } +} + +void +ClientStore::clear() +{ + Store::clear(); + _plugins->clear(); +} + +void +ClientStore::add_object(SPtr<ObjectModel> object) +{ + // If we already have "this" object, merge the existing one into the new + // one (with precedence to the new values). + auto existing = find(object->path()); + if (existing != end()) { + dynamic_ptr_cast<ObjectModel>(existing->second)->set(object); + } else { + if (!object->path().is_root()) { + SPtr<ObjectModel> parent = _object(object->path().parent()); + if (parent) { + assert(object->path().is_child_of(parent->path())); + object->set_parent(parent); + parent->add_child(object); + assert(parent && (object->parent() == parent)); + + (*this)[object->path()] = object; + _signal_new_object.emit(object); + } else { + _log.error(fmt("Object %1% with no parent\n") % object->path()); + } + } else { + (*this)[object->path()] = object; + _signal_new_object.emit(object); + } + } + + for (auto p : object->properties()) { + object->signal_property().emit(p.first, p.second); + } +} + +SPtr<ObjectModel> +ClientStore::remove_object(const Raul::Path& path) +{ + // Find the object, the "top" of the tree to remove + const iterator top = find(path); + if (top == end()) { + return SPtr<ObjectModel>(); + } + + SPtr<ObjectModel> object = dynamic_ptr_cast<ObjectModel>(top->second); + + // Remove object and any adjacent arcs from parent if applicable + if (object && object->parent()) { + SPtr<PortModel> port = dynamic_ptr_cast<PortModel>(object); + if (port && dynamic_ptr_cast<GraphModel>(port->parent())) { + disconnect_all(port->parent()->path(), path); + if (port->parent()->parent()) { + disconnect_all(port->parent()->parent()->path(), path); + } + } else if (port && port->parent()->parent()) { + disconnect_all(port->parent()->parent()->path(), path); + } else { + disconnect_all(object->parent()->path(), path); + } + + object->parent()->remove_child(object); + } + + // Remove the object and all its descendants + Objects removed; + remove(top, removed); + + // Notify everything that needs to know this object has been removed + if (object) { + object->signal_destroyed().emit(); + } + + return object; +} + +SPtr<PluginModel> +ClientStore::_plugin(const URI& uri) +{ + const Plugins::iterator i = _plugins->find(uri); + return (i == _plugins->end()) ? SPtr<PluginModel>() : (*i).second; +} + +SPtr<PluginModel> +ClientStore::_plugin(const Atom& uri) +{ + /* FIXME: Should probably be stored with URIs rather than strings, to make + this a fast case. */ + + const Plugins::iterator i = _plugins->find(URI(_uris.forge.str(uri, false))); + return (i == _plugins->end()) ? SPtr<PluginModel>() : (*i).second; +} + +SPtr<const PluginModel> +ClientStore::plugin(const URI& uri) const +{ + return const_cast<ClientStore*>(this)->_plugin(uri); +} + +SPtr<ObjectModel> +ClientStore::_object(const Raul::Path& path) +{ + const iterator i = find(path); + if (i == end()) { + return SPtr<ObjectModel>(); + } else { + SPtr<ObjectModel> model = dynamic_ptr_cast<ObjectModel>(i->second); + assert(model); + assert(model->path().is_root() || model->parent()); + return model; + } +} + +SPtr<const ObjectModel> +ClientStore::object(const Raul::Path& path) const +{ + return const_cast<ClientStore*>(this)->_object(path); +} + +SPtr<Resource> +ClientStore::_resource(const URI& uri) +{ + if (uri_is_path(uri)) { + return _object(uri_to_path(uri)); + } else { + return _plugin(uri); + } +} + +SPtr<const Resource> +ClientStore::resource(const URI& uri) const +{ + return const_cast<ClientStore*>(this)->_resource(uri); +} + +void +ClientStore::add_plugin(SPtr<PluginModel> pm) +{ + SPtr<PluginModel> existing = _plugin(pm->uri()); + if (existing) { + existing->set(pm); + } else { + _plugins->emplace(pm->uri(), pm); + _signal_new_plugin.emit(pm); + } +} + +/* ****** Signal Handlers ******** */ + +void +ClientStore::operator()(const Del& del) +{ + if (uri_is_path(del.uri)) { + remove_object(uri_to_path(del.uri)); + } else { + auto p = _plugins->find(del.uri); + if (p != _plugins->end()) { + _plugins->erase(p); + _signal_plugin_deleted.emit(del.uri); + } + } +} + +void +ClientStore::operator()(const Copy&) +{ + _log.error("Client store copy unsupported\n"); +} + +void +ClientStore::operator()(const Move& msg) +{ + const iterator top = find(msg.old_path); + if (top != end()) { + rename(top, msg.new_path); + } +} + +void +ClientStore::message(const Message& msg) +{ + boost::apply_visitor(*this, msg); +} + +void +ClientStore::operator()(const Put& msg) +{ + typedef Properties::const_iterator Iterator; + + const auto& uri = msg.uri; + const auto& properties = msg.properties; + + bool is_graph, is_block, is_port, is_output; + Resource::type(uris(), properties, + is_graph, is_block, is_port, is_output); + + // Check for specially handled types + const Iterator t = properties.find(_uris.rdf_type); + if (t != properties.end()) { + const Atom& type(t->second); + if (_uris.pset_Preset == type) { + const Iterator p = properties.find(_uris.lv2_appliesTo); + const Iterator l = properties.find(_uris.rdfs_label); + SPtr<PluginModel> plug; + if (p == properties.end()) { + _log.error(fmt("Preset <%1%> with no plugin\n") % uri.c_str()); + } else if (l == properties.end()) { + _log.error(fmt("Preset <%1%> with no label\n") % uri.c_str()); + } else if (l->second.type() != _uris.forge.String) { + _log.error(fmt("Preset <%1%> label is not a string\n") % uri.c_str()); + } else if (!(plug = _plugin(p->second))) { + _log.error(fmt("Preset <%1%> for unknown plugin %2%\n") + % uri.c_str() % _uris.forge.str(p->second, true)); + } else { + plug->add_preset(uri, l->second.ptr<char>()); + } + return; + } else if (_uris.ingen_Graph == type) { + is_graph = true; + } else if (_uris.ingen_Internal == type || _uris.lv2_Plugin == type) { + SPtr<PluginModel> p(new PluginModel(uris(), uri, type, properties)); + add_plugin(p); + return; + } + } + + if (!uri_is_path(uri)) { + _log.error(fmt("Put for unknown subject <%1%>\n") + % uri.c_str()); + return; + } + + const Raul::Path path(uri_to_path(uri)); + + SPtr<ObjectModel> obj = dynamic_ptr_cast<ObjectModel>(_object(path)); + if (obj) { + obj->set_properties(properties); + return; + } + + if (path.is_root()) { + is_graph = true; + } + + if (is_graph) { + SPtr<GraphModel> model(new GraphModel(uris(), path)); + model->set_properties(properties); + add_object(model); + } else if (is_block) { + auto p = properties.find(_uris.lv2_prototype); + if (p == properties.end()) { + p = properties.find(_uris.ingen_prototype); + } + + SPtr<PluginModel> plug; + if (p->second.is_valid() && (p->second.type() == _uris.forge.URI || + p->second.type() == _uris.forge.URID)) { + const URI uri(_uris.forge.str(p->second, false)); + if (!(plug = _plugin(uri))) { + plug = SPtr<PluginModel>( + new PluginModel(uris(), uri, Atom(), Properties())); + add_plugin(plug); + } + + SPtr<BlockModel> bm(new BlockModel(uris(), plug, path)); + bm->set_properties(properties); + add_object(bm); + } else { + _log.warn(fmt("Block %1% has no prototype\n") % path.c_str()); + } + } else if (is_port) { + PortModel::Direction pdir = (is_output) + ? PortModel::Direction::OUTPUT + : PortModel::Direction::INPUT; + uint32_t index = 0; + const Iterator i = properties.find(_uris.lv2_index); + if (i != properties.end() && i->second.type() == _uris.forge.Int) { + index = i->second.get<int32_t>(); + } + + SPtr<PortModel> p(new PortModel(uris(), path, index, pdir)); + p->set_properties(properties); + add_object(p); + } else { + _log.warn(fmt("Ignoring %1% of unknown type\n") % path.c_str()); + } +} + +void +ClientStore::operator()(const Delta& msg) +{ + const auto& uri = msg.uri; + if (uri == URI("ingen:/clients/this")) { + // Client property, which we don't store (yet?) + return; + } + + if (!uri_is_path(uri)) { + _log.error(fmt("Delta for unknown subject <%1%>\n") + % uri.c_str()); + return; + } + + const Raul::Path path(uri_to_path(uri)); + + SPtr<ObjectModel> obj = _object(path); + if (obj) { + obj->remove_properties(msg.remove); + obj->add_properties(msg.add); + } else { + _log.warn(fmt("Failed to find object `%1%'\n") % path.c_str()); + } +} + +void +ClientStore::operator()(const SetProperty& msg) +{ + const auto& subject_uri = msg.subject; + const auto& predicate = msg.predicate; + const auto& value = msg.value; + + if (subject_uri == URI("ingen:/engine")) { + _log.info(fmt("Engine property <%1%> = %2%\n") + % predicate.c_str() % _uris.forge.str(value, false)); + return; + } + SPtr<Resource> subject = _resource(subject_uri); + if (subject) { + if (predicate == _uris.ingen_activity) { + /* Activity is transient, trigger any live actions (like GUI + blinkenlights) but do not store the property. */ + subject->on_property(predicate, value); + } else { + subject->set_property(predicate, value, msg.ctx); + } + } else { + SPtr<PluginModel> plugin = _plugin(subject_uri); + if (plugin) { + plugin->set_property(predicate, value); + } else if (predicate != _uris.ingen_activity) { + _log.warn(fmt("Property <%1%> for unknown object %2%\n") + % predicate.c_str() % subject_uri.c_str()); + } + } +} + +SPtr<GraphModel> +ClientStore::connection_graph(const Raul::Path& tail_path, + const Raul::Path& head_path) +{ + SPtr<GraphModel> graph; + + if (tail_path.parent() == head_path.parent()) { + graph = dynamic_ptr_cast<GraphModel>(_object(tail_path.parent())); + } + + if (!graph && tail_path.parent() == head_path.parent().parent()) { + graph = dynamic_ptr_cast<GraphModel>(_object(tail_path.parent())); + } + + if (!graph && tail_path.parent().parent() == head_path.parent()) { + graph = dynamic_ptr_cast<GraphModel>(_object(head_path.parent())); + } + + if (!graph) { + graph = dynamic_ptr_cast<GraphModel>(_object(tail_path.parent().parent())); + } + + if (!graph) { + _log.error(fmt("Unable to find graph for arc %1% => %2%\n") + % tail_path % head_path); + } + + return graph; +} + +bool +ClientStore::attempt_connection(const Raul::Path& tail_path, + const Raul::Path& head_path) +{ + SPtr<PortModel> tail = dynamic_ptr_cast<PortModel>(_object(tail_path)); + SPtr<PortModel> head = dynamic_ptr_cast<PortModel>(_object(head_path)); + + if (tail && head) { + SPtr<GraphModel> graph = connection_graph(tail_path, head_path); + SPtr<ArcModel> arc(new ArcModel(tail, head)); + graph->add_arc(arc); + return true; + } else { + _log.warn(fmt("Failed to connect %1% => %2%\n") + % tail_path % head_path); + return false; + } +} + +void +ClientStore::operator()(const Connect& msg) +{ + attempt_connection(msg.tail, msg.head); +} + +void +ClientStore::operator()(const Disconnect& msg) +{ + SPtr<PortModel> tail = dynamic_ptr_cast<PortModel>(_object(msg.tail)); + SPtr<PortModel> head = dynamic_ptr_cast<PortModel>(_object(msg.head)); + SPtr<GraphModel> graph = connection_graph(msg.tail, msg.head); + if (graph) { + graph->remove_arc(tail.get(), head.get()); + } +} + +void +ClientStore::operator()(const DisconnectAll& msg) +{ + SPtr<GraphModel> graph = dynamic_ptr_cast<GraphModel>(_object(msg.graph)); + SPtr<ObjectModel> object = _object(msg.path); + + if (!graph || !object) { + _log.error(fmt("Bad disconnect all notification %1% in %2%\n") + % msg.path % msg.graph); + return; + } + + const GraphModel::Arcs arcs = graph->arcs(); + for (auto a : arcs) { + SPtr<ArcModel> arc = dynamic_ptr_cast<ArcModel>(a.second); + if (arc->tail()->parent() == object + || arc->head()->parent() == object + || arc->tail()->path() == msg.path + || arc->head()->path() == msg.path) { + graph->remove_arc(arc->tail().get(), arc->head().get()); + } + } +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/GraphModel.cpp b/src/client/GraphModel.cpp new file mode 100644 index 00000000..0723e59b --- /dev/null +++ b/src/client/GraphModel.cpp @@ -0,0 +1,176 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> + +#include "ingen/URIs.hpp" +#include "ingen/client/ArcModel.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +namespace Ingen { +namespace Client { + +void +GraphModel::add_child(SPtr<ObjectModel> c) +{ + assert(c->parent().get() == this); + + SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(c); + if (pm) { + add_port(pm); + return; + } + + SPtr<BlockModel> bm = dynamic_ptr_cast<BlockModel>(c); + if (bm) { + _signal_new_block.emit(bm); + } +} + +bool +GraphModel::remove_child(SPtr<ObjectModel> o) +{ + assert(o->path().is_child_of(path())); + assert(o->parent().get() == this); + + SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(o); + if (pm) { + remove_arcs_on(pm); + remove_port(pm); + } + + SPtr<BlockModel> bm = dynamic_ptr_cast<BlockModel>(o); + if (bm) { + _signal_removed_block.emit(bm); + } + + return true; +} + +void +GraphModel::remove_arcs_on(SPtr<PortModel> p) +{ + // Remove any connections which referred to this object, + // since they can't possibly exist anymore + for (auto j = _arcs.begin(); j != _arcs.end();) { + auto next = j; + ++next; + + SPtr<ArcModel> arc = dynamic_ptr_cast<ArcModel>(j->second); + if (arc->tail_path().parent() == p->path() + || arc->tail_path() == p->path() + || arc->head_path().parent() == p->path() + || arc->head_path() == p->path()) { + _signal_removed_arc.emit(arc); + _arcs.erase(j); // Cuts our reference + } + j = next; + } +} + +void +GraphModel::clear() +{ + _arcs.clear(); + + BlockModel::clear(); + + assert(_arcs.empty()); + assert(_ports.empty()); +} + +SPtr<ArcModel> +GraphModel::get_arc(const Node* tail, const Node* head) +{ + auto i = _arcs.find(std::make_pair(tail, head)); + if (i != _arcs.end()) { + return dynamic_ptr_cast<ArcModel>(i->second); + } else { + return SPtr<ArcModel>(); + } +} + +/** Add a connection to this graph. + * + * A reference to `arc` is taken, released on deletion or removal. + * If `arc` only contains paths (not pointers to the actual ports), the ports + * will be found and set. The ports referred to not existing as children of + * this graph is a fatal error. + */ +void +GraphModel::add_arc(SPtr<ArcModel> arc) +{ + // Store should have 'resolved' the connection already + assert(arc); + assert(arc->tail()); + assert(arc->head()); + assert(arc->tail()->parent()); + assert(arc->head()->parent()); + assert(arc->tail_path() != arc->head_path()); + assert(arc->tail()->parent().get() == this + || arc->tail()->parent()->parent().get() == this); + assert(arc->head()->parent().get() == this + || arc->head()->parent()->parent().get() == this); + + SPtr<ArcModel> existing = get_arc( + arc->tail().get(), arc->head().get()); + + if (existing) { + assert(arc->tail() == existing->tail()); + assert(arc->head() == existing->head()); + } else { + _arcs.emplace(std::make_pair(arc->tail().get(), arc->head().get()), + arc); + _signal_new_arc.emit(arc); + } +} + +void +GraphModel::remove_arc(const Node* tail, const Node* head) +{ + auto i = _arcs.find(std::make_pair(tail, head)); + if (i != _arcs.end()) { + SPtr<ArcModel> arc = dynamic_ptr_cast<ArcModel>(i->second); + _signal_removed_arc.emit(arc); + _arcs.erase(i); + } +} + +bool +GraphModel::enabled() const +{ + const Atom& enabled = get_property(_uris.ingen_enabled); + return (enabled.is_valid() && enabled.get<int32_t>()); +} + +uint32_t +GraphModel::internal_poly() const +{ + const Atom& poly = get_property(_uris.ingen_polyphony); + return poly.is_valid() ? poly.get<int32_t>() : 1; +} + +bool +GraphModel::polyphonic() const +{ + const Atom& poly = get_property(_uris.ingen_polyphonic); + return poly.is_valid() && poly.get<int32_t>(); +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/ObjectModel.cpp b/src/client/ObjectModel.cpp new file mode 100644 index 00000000..8d40b120 --- /dev/null +++ b/src/client/ObjectModel.cpp @@ -0,0 +1,108 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Node.hpp" +#include "ingen/URIs.hpp" +#include "ingen/client/ObjectModel.hpp" + +namespace Ingen { +namespace Client { + +ObjectModel::ObjectModel(URIs& uris, const Raul::Path& path) + : Node(uris, path) + , _path(path) + , _symbol((path == "/") ? "root" : path.symbol()) +{ +} + +ObjectModel::ObjectModel(const ObjectModel& copy) + : Node(copy) + , _parent(copy._parent) + , _path(copy._path) + , _symbol(copy._symbol) +{ +} + +bool +ObjectModel::is_a(const URIs::Quark& type) const +{ + return has_property(_uris.rdf_type, type); +} + +void +ObjectModel::on_property(const URI& uri, const Atom& value) +{ + _signal_property.emit(uri, value); +} + +void +ObjectModel::on_property_removed(const URI& uri, const Atom& value) +{ + _signal_property_removed.emit(uri, value); +} + +const Atom& +ObjectModel::get_property(const URI& key) const +{ + static const Atom null_atom; + auto i = properties().find(key); + return (i != properties().end()) ? i->second : null_atom; +} + +bool +ObjectModel::polyphonic() const +{ + const Atom& polyphonic = get_property(_uris.ingen_polyphonic); + return (polyphonic.is_valid() && polyphonic.get<int32_t>()); +} + +/** Merge the data of `o` with self, as much as possible. + * + * This will merge the two models, but with any conflict take the value in + * `o` as correct. The paths of the two models MUST be equal. + */ +void +ObjectModel::set(SPtr<ObjectModel> o) +{ + assert(_path == o->path()); + if (o->_parent) { + _parent = o->_parent; + } + + for (auto v : o->properties()) { + Resource::set_property(v.first, v.second); + _signal_property.emit(v.first, v.second); + } +} + +void +ObjectModel::set_path(const Raul::Path& p) +{ + _path = p; + _symbol = Raul::Symbol(p.is_root() ? "root" : p.symbol()); + set_uri(path_to_uri(p)); + _signal_moved.emit(); +} + +void +ObjectModel::set_parent(SPtr<ObjectModel> p) +{ + assert(_path.is_child_of(p->path())); + _parent = p; +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/PluginModel.cpp b/src/client/PluginModel.cpp new file mode 100644 index 00000000..5427b75e --- /dev/null +++ b/src/client/PluginModel.cpp @@ -0,0 +1,360 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> +#include <algorithm> + +#include <boost/optional.hpp> + +#include "raul/Path.hpp" + +#include "ingen/Atom.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/client/PluginUI.hpp" + +#include "ingen_config.h" + +using std::string; + +namespace Ingen { +namespace Client { + +LilvWorld* PluginModel::_lilv_world = nullptr; +const LilvPlugins* PluginModel::_lilv_plugins = nullptr; + +Sord::World* PluginModel::_rdf_world = nullptr; + +PluginModel::PluginModel(URIs& uris, + const URI& uri, + const Atom& type, + const Properties& properties) + : Resource(uris, uri) + , _type(type) + , _fetched(false) +{ + if (!_type.is_valid()) { + if (uri.string().find("ingen-internals") != string::npos) { + _type = uris.ingen_Internal.urid; + } else { + _type = uris.lv2_Plugin.urid; // Assume LV2 and hope for the best... + } + } + + add_property(uris.rdf_type, type); + add_properties(properties); + + LilvNode* plugin_uri = lilv_new_uri(_lilv_world, uri.c_str()); + _lilv_plugin = lilv_plugins_get_by_uri(_lilv_plugins, plugin_uri); + lilv_node_free(plugin_uri); + + if (uris.ingen_Internal == _type) { + set_property(uris.doap_name, + uris.forge.alloc(std::string(uri.fragment().substr(1)))); + } +} + +static size_t +last_uri_delim(const std::string& str) +{ + for (size_t i = str.length() - 1; i > 0; --i) { + switch (str[i]) { + case ':': case '/': case '?': case '#': + return i; + } + } + return string::npos; +} + +static bool +contains_alpha_after(const std::string& str, size_t begin) +{ + for (size_t i = begin; i < str.length(); ++i) { + if (isalpha(str[i])) { + return true; + } + } + return false; +} + +const Atom& +PluginModel::get_property(const URI& key) const +{ + static const Atom nil; + const Atom& val = Resource::get_property(key); + if (val.is_valid()) { + return val; + } + + // No lv2:symbol from data or engine, invent one + if (key == _uris.lv2_symbol) { + string str = this->uri(); + size_t last_delim = last_uri_delim(str); + while (last_delim != string::npos && + !contains_alpha_after(str, last_delim)) { + str = str.substr(0, last_delim); + last_delim = last_uri_delim(str); + } + str = str.substr(last_delim + 1); + + std::string symbol = Raul::Symbol::symbolify(str); + set_property(_uris.lv2_symbol, _uris.forge.alloc(symbol)); + return get_property(key); + } + + if (_lilv_plugin) { + boost::optional<const Atom&> ret; + LilvNode* lv2_pred = lilv_new_uri(_lilv_world, key.c_str()); + LilvNodes* values = lilv_plugin_get_value(_lilv_plugin, lv2_pred); + lilv_node_free(lv2_pred); + LILV_FOREACH(nodes, i, values) { + const LilvNode* val = lilv_nodes_get(values, i); + if (lilv_node_is_uri(val)) { + ret = set_property( + key, _uris.forge.make_urid(URI(lilv_node_as_uri(val)))); + break; + } else if (lilv_node_is_string(val)) { + ret = set_property( + key, _uris.forge.alloc(lilv_node_as_string(val))); + break; + } else if (lilv_node_is_float(val)) { + ret = set_property( + key, _uris.forge.make(lilv_node_as_float(val))); + break; + } else if (lilv_node_is_int(val)) { + ret = set_property( + key, _uris.forge.make(lilv_node_as_int(val))); + break; + } + } + lilv_nodes_free(values); + + if (ret) { + return *ret; + } + } + + return nil; +} + +void +PluginModel::set(SPtr<PluginModel> p) +{ + _type = p->_type; + + if (p->_lilv_plugin) { + _lilv_plugin = p->_lilv_plugin; + } + + for (auto v : p->properties()) { + Resource::set_property(v.first, v.second); + _signal_property.emit(v.first, v.second); + } + + _signal_changed.emit(); +} + +void +PluginModel::add_preset(const URI& uri, const std::string& label) +{ + _presets.emplace(uri, label); + _signal_preset.emit(uri, label); +} + +Raul::Symbol +PluginModel::default_block_symbol() const +{ + const Atom& name_atom = get_property(_uris.lv2_symbol); + if (name_atom.is_valid() && name_atom.type() == _uris.forge.String) { + return Raul::Symbol::symbolify(name_atom.ptr<char>()); + } else { + return Raul::Symbol("_"); + } +} + +string +PluginModel::human_name() const +{ + const Atom& name_atom = get_property(_uris.doap_name); + if (name_atom.type() == _uris.forge.String) { + return name_atom.ptr<char>(); + } else { + return default_block_symbol().c_str(); + } +} + +string +PluginModel::port_human_name(uint32_t i) const +{ + if (_lilv_plugin) { + const LilvPort* port = lilv_plugin_get_port_by_index(_lilv_plugin, i); + LilvNode* name = lilv_port_get_name(_lilv_plugin, port); + const string ret(lilv_node_as_string(name)); + lilv_node_free(name); + return ret; + } + return ""; +} + +PluginModel::ScalePoints +PluginModel::port_scale_points(uint32_t i) const +{ + // TODO: Non-float scale points + ScalePoints points; + if (_lilv_plugin) { + const LilvPort* port = lilv_plugin_get_port_by_index(_lilv_plugin, i); + LilvScalePoints* sp = lilv_port_get_scale_points(_lilv_plugin, port); + LILV_FOREACH(scale_points, i, sp) { + const LilvScalePoint* p = lilv_scale_points_get(sp, i); + points.emplace( + lilv_node_as_float(lilv_scale_point_get_value(p)), + lilv_node_as_string(lilv_scale_point_get_label(p))); + } + } + return points; +} + +bool +PluginModel::has_ui() const +{ + if (_lilv_plugin) { + LilvUIs* uis = lilv_plugin_get_uis(_lilv_plugin); + const bool ret = (lilv_nodes_size(uis) > 0); + lilv_uis_free(uis); + return ret; + } + return false; +} + +SPtr<PluginUI> +PluginModel::ui(Ingen::World* world, + SPtr<const BlockModel> block) const +{ + if (!_lilv_plugin) { + return SPtr<PluginUI>(); + } + + return PluginUI::create(world, block, _lilv_plugin); +} + +static std::string +heading(const std::string& text, bool html, unsigned level) +{ + if (html) { + const std::string tag = std::string("h") + std::to_string(level); + return std::string("<") + tag + ">" + text + "</" + tag + ">\n"; + } else { + return text + ":\n\n"; + } +} + +static std::string +link(const std::string& addr, bool html) +{ + if (html) { + return std::string("<a href=\"") + addr + "\">" + addr + "</a>"; + } else { + return addr; + } +} + +std::string +PluginModel::get_documentation(const LilvNode* subject, bool html) const +{ + std::string doc; + + LilvNode* lv2_documentation = lilv_new_uri(_lilv_world, + LV2_CORE__documentation); + LilvNode* rdfs_comment = lilv_new_uri(_lilv_world, + LILV_NS_RDFS "comment"); + + LilvNodes* vals = lilv_world_find_nodes( + _lilv_world, subject, lv2_documentation, nullptr); + const bool doc_is_html = vals; + if (!vals) { + vals = lilv_world_find_nodes( + _lilv_world, subject, rdfs_comment, nullptr); + } + + if (vals) { + const LilvNode* val = lilv_nodes_get_first(vals); + if (lilv_node_is_string(val)) { + doc += lilv_node_as_string(val); + } + } + + if (html && !doc_is_html) { + for (std::size_t i = 0; i < doc.size(); ++i) { + if (doc.substr(i, 2) == "\n\n") { + doc.replace(i, 2, "<br/><br/>"); + i += strlen("<br/><br/>"); + } + } + } + + lilv_node_free(rdfs_comment); + lilv_node_free(lv2_documentation); + + return doc; +} + +std::string +PluginModel::documentation(const bool html) const +{ + LilvNode* subject = (_lilv_plugin) + ? lilv_node_duplicate(lilv_plugin_get_uri(_lilv_plugin)) + : lilv_new_uri(_lilv_world, uri().c_str()); + + const std::string doc(get_documentation(subject, html)); + + lilv_node_free(subject); + + return (heading(human_name(), html, 2) + + link(uri(), html) + (html ? "<br/><br/>" : "\n\n") + + doc); +} + +std::string +PluginModel::port_documentation(uint32_t index, bool html) const +{ + if (!_lilv_plugin) { + return ""; + } + + const LilvPort* port = lilv_plugin_get_port_by_index(_lilv_plugin, index); + if (!port) { + return ""; + } + + return (heading(port_human_name(index), html, 2) + + get_documentation(lilv_port_get_node(_lilv_plugin, port), html)); +} + +const LilvPort* +PluginModel::lilv_port(uint32_t index) const +{ + return lilv_plugin_get_port_by_index(_lilv_plugin, index); +} + +void +PluginModel::set_lilv_world(LilvWorld* world) +{ + _lilv_world = world; + _lilv_plugins = lilv_world_get_all_plugins(_lilv_world); +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/PluginUI.cpp b/src/client/PluginUI.cpp new file mode 100644 index 00000000..df983f7f --- /dev/null +++ b/src/client/PluginUI.cpp @@ -0,0 +1,336 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/PluginUI.hpp" +#include "ingen/client/PortModel.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/extensions/ui/ui.h" + +namespace Ingen { +namespace Client { + +SuilHost* PluginUI::ui_host = nullptr; + +static SPtr<const PortModel> +get_port(PluginUI* ui, uint32_t port_index) +{ + if (port_index >= ui->block()->ports().size()) { + ui->world()->log().error( + fmt("%1% UI tried to access invalid port %2%\n") + % ui->block()->plugin()->uri().c_str() % port_index); + return SPtr<const PortModel>(); + } + return ui->block()->ports()[port_index]; +} + +static void +lv2_ui_write(SuilController controller, + uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void* buffer) +{ + PluginUI* const ui = (PluginUI*)controller; + const URIs& uris = ui->world()->uris(); + SPtr<const PortModel> port = get_port(ui, port_index); + if (!port) { + return; + } + + // float (special case, always 0) + if (format == 0) { + if (buffer_size != 4) { + ui->world()->log().error( + fmt("%1% UI wrote corrupt float with bad size\n") + % ui->block()->plugin()->uri().c_str()); + return; + } + const float value = *(const float*)buffer; + if (port->value().type() == uris.atom_Float && + value == port->value().get<float>()) { + return; // Ignore feedback + } + + ui->signal_property_changed()( + port->uri(), + uris.ingen_value, + ui->world()->forge().make(value), + Resource::Graph::DEFAULT); + + } else if (format == uris.atom_eventTransfer.urid.get<LV2_URID>()) { + const LV2_Atom* atom = (const LV2_Atom*)buffer; + Atom val = ui->world()->forge().alloc( + atom->size, atom->type, LV2_ATOM_BODY_CONST(atom)); + ui->signal_property_changed()(port->uri(), + uris.ingen_activity, + val, + Resource::Graph::DEFAULT); + } else { + ui->world()->log().warn( + fmt("Unknown value format %1% from LV2 UI\n") + % format % ui->block()->plugin()->uri().c_str()); + } +} + +static uint32_t +lv2_ui_port_index(SuilController controller, const char* port_symbol) +{ + PluginUI* const ui = (PluginUI*)controller; + + const BlockModel::Ports& ports = ui->block()->ports(); + for (uint32_t i = 0; i < ports.size(); ++i) { + if (ports[i]->symbol() == port_symbol) { + return i; + } + } + return LV2UI_INVALID_PORT_INDEX; +} + +static uint32_t +lv2_ui_subscribe(SuilController controller, + uint32_t port_index, + uint32_t protocol, + const LV2_Feature* const* features) +{ + PluginUI* const ui = (PluginUI*)controller; + SPtr<const PortModel> port = get_port(ui, port_index); + if (!port) { + return 1; + } + + ui->signal_property_changed()( + ui->block()->ports()[port_index]->uri(), + ui->world()->uris().ingen_broadcast, + ui->world()->forge().make(true), + Resource::Graph::DEFAULT); + + return 0; +} + +static uint32_t +lv2_ui_unsubscribe(SuilController controller, + uint32_t port_index, + uint32_t protocol, + const LV2_Feature* const* features) +{ + PluginUI* const ui = (PluginUI*)controller; + SPtr<const PortModel> port = get_port(ui, port_index); + if (!port) { + return 1; + } + + ui->signal_property_changed()( + ui->block()->ports()[port_index]->uri(), + ui->world()->uris().ingen_broadcast, + ui->world()->forge().make(false), + Resource::Graph::DEFAULT); + + return 0; +} + +PluginUI::PluginUI(Ingen::World* world, + SPtr<const BlockModel> block, + LilvUIs* uis, + const LilvUI* ui, + const LilvNode* ui_type) + : _world(world) + , _block(std::move(block)) + , _instance(nullptr) + , _uis(uis) + , _ui(ui) + , _ui_node(lilv_node_duplicate(lilv_ui_get_uri(ui))) + , _ui_type(lilv_node_duplicate(ui_type)) +{ +} + +PluginUI::~PluginUI() +{ + for (uint32_t i : _subscribed_ports) { + lv2_ui_unsubscribe(this, i, 0, nullptr); + } + suil_instance_free(_instance); + lilv_node_free(_ui_node); + lilv_node_free(_ui_type); + lilv_uis_free(_uis); + lilv_world_unload_resource(_world->lilv_world(), lilv_ui_get_uri(_ui)); +} + +SPtr<PluginUI> +PluginUI::create(Ingen::World* world, + SPtr<const BlockModel> block, + const LilvPlugin* plugin) +{ + if (!PluginUI::ui_host) { + PluginUI::ui_host = suil_host_new(lv2_ui_write, + lv2_ui_port_index, + lv2_ui_subscribe, + lv2_ui_unsubscribe); + } + + static const char* gtk_ui_uri = LV2_UI__GtkUI; + + LilvNode* gtk_ui = lilv_new_uri(world->lilv_world(), gtk_ui_uri); + + LilvUIs* uis = lilv_plugin_get_uis(plugin); + const LilvUI* ui = nullptr; + const LilvNode* ui_type = nullptr; + LILV_FOREACH(uis, u, uis) { + const LilvUI* this_ui = lilv_uis_get(uis, u); + if (lilv_ui_is_supported(this_ui, + suil_ui_supported, + gtk_ui, + &ui_type)) { + // TODO: Multiple UI support + ui = this_ui; + break; + } + } + + if (!ui) { + lilv_node_free(gtk_ui); + return SPtr<PluginUI>(); + } + + // Create the PluginUI, but don't instantiate yet + SPtr<PluginUI> ret(new PluginUI(world, block, uis, ui, ui_type)); + ret->_features = world->lv2_features().lv2_features( + world, const_cast<BlockModel*>(block.get())); + + return ret; +} + +bool +PluginUI::instantiate() +{ + const URIs& uris = _world->uris(); + const std::string plugin_uri = _block->plugin()->uri(); + LilvWorld* lworld = _world->lilv_world(); + + // Load seeAlso files to access data like portNotification descriptions + lilv_world_load_resource(lworld, lilv_ui_get_uri(_ui)); + + /* Subscribe (enable broadcast) for any requested port notifications. This + must be done before instantiation so responses to any events sent by the + UI's init() will be sent back to this client. */ + LilvNode* ui_portNotification = lilv_new_uri(lworld, LV2_UI__portNotification); + LilvNode* ui_plugin = lilv_new_uri(lworld, LV2_UI__plugin); + LilvNodes* notes = lilv_world_find_nodes( + lworld, lilv_ui_get_uri(_ui), ui_portNotification, nullptr); + LILV_FOREACH(nodes, n, notes) { + const LilvNode* note = lilv_nodes_get(notes, n); + const LilvNode* sym = lilv_world_get(lworld, note, uris.lv2_symbol, nullptr); + const LilvNode* plug = lilv_world_get(lworld, note, ui_plugin, nullptr); + if (!plug) { + _world->log().error(fmt("%1% UI %2% notification missing plugin\n") + % plugin_uri % lilv_node_as_string(_ui_node)); + } else if (!sym) { + _world->log().error(fmt("%1% UI %2% notification missing symbol\n") + % plugin_uri % lilv_node_as_string(_ui_node)); + } else if (!lilv_node_is_uri(plug)) { + _world->log().error(fmt("%1% UI %2% notification has non-URI plugin\n") + % plugin_uri % lilv_node_as_string(_ui_node)); + } else if (!strcmp(lilv_node_as_uri(plug), plugin_uri.c_str())) { + // Notification is valid and for this plugin + uint32_t index = lv2_ui_port_index(this, lilv_node_as_string(sym)); + if (index != LV2UI_INVALID_PORT_INDEX) { + lv2_ui_subscribe(this, index, 0, nullptr); + _subscribed_ports.insert(index); + } + } + } + lilv_nodes_free(notes); + lilv_node_free(ui_plugin); + lilv_node_free(ui_portNotification); + + const char* bundle_uri = lilv_node_as_uri(lilv_ui_get_bundle_uri(_ui)); + const char* binary_uri = lilv_node_as_uri(lilv_ui_get_binary_uri(_ui)); + char* bundle_path = lilv_file_uri_parse(bundle_uri, nullptr); + char* binary_path = lilv_file_uri_parse(binary_uri, nullptr); + + // Instantiate the actual plugin UI via Suil + _instance = suil_instance_new( + PluginUI::ui_host, + this, + LV2_UI__GtkUI, + plugin_uri.c_str(), + lilv_node_as_uri(lilv_ui_get_uri(_ui)), + lilv_node_as_uri(_ui_type), + bundle_path, + binary_path, + _features->array()); + + lilv_free(binary_path); + lilv_free(bundle_path); + + if (!_instance) { + _world->log().error("Failed to instantiate LV2 UI\n"); + // Cancel any subscriptions + for (uint32_t i : _subscribed_ports) { + lv2_ui_unsubscribe(this, i, 0, nullptr); + } + return false; + } + + return true; +} + +SuilWidget +PluginUI::get_widget() +{ + return (SuilWidget*)suil_instance_get_widget(_instance); +} + +void +PluginUI::port_event(uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void* buffer) +{ + if (_instance) { + suil_instance_port_event( + _instance, port_index, buffer_size, format, buffer); + } else { + _world->log().warn("LV2 UI port event with no instance\n"); + } +} + +bool +PluginUI::is_resizable() const +{ + LilvWorld* w = _world->lilv_world(); + const LilvNode* s = _ui_node; + LilvNode* p = lilv_new_uri(w, LV2_CORE__optionalFeature); + LilvNode* fs = lilv_new_uri(w, LV2_UI__fixedSize); + LilvNode* nrs = lilv_new_uri(w, LV2_UI__noUserResize); + + LilvNodes* fs_matches = lilv_world_find_nodes(w, s, p, fs); + LilvNodes* nrs_matches = lilv_world_find_nodes(w, s, p, nrs); + + lilv_nodes_free(nrs_matches); + lilv_nodes_free(fs_matches); + lilv_node_free(nrs); + lilv_node_free(fs); + lilv_node_free(p); + + return !fs_matches && !nrs_matches; +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/PortModel.cpp b/src/client/PortModel.cpp new file mode 100644 index 00000000..5c9a8c77 --- /dev/null +++ b/src/client/PortModel.cpp @@ -0,0 +1,78 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/PortModel.hpp" + +namespace Ingen { +namespace Client { + +void +PortModel::on_property(const URI& uri, const Atom& value) +{ + if (uri == _uris.ingen_activity) { + // Don't store activity, it is transient + signal_activity().emit(value); + return; + } + + ObjectModel::on_property(uri, value); + + if (uri == _uris.ingen_value) { + signal_value_changed().emit(value); + } +} + +bool +PortModel::supports(const URIs::Quark& value_type) const +{ + return has_property(_uris.atom_supports, value_type); +} + +bool +PortModel::port_property(const URIs::Quark& uri) const +{ + return has_property(_uris.lv2_portProperty, uri); +} + +bool +PortModel::is_uri() const +{ + // FIXME: Resource::has_property doesn't work, URI != URID + for (auto p : properties()) { + if (p.second.type() == _uris.atom_URID && + static_cast<LV2_URID>(p.second.get<int32_t>()) == _uris.atom_URID) { + return true; + } + } + return false; +} + +void +PortModel::set(SPtr<ObjectModel> model) +{ + ObjectModel::set(model); + + SPtr<PortModel> port = dynamic_ptr_cast<PortModel>(model); + if (port) { + _index = port->_index; + _direction = port->_direction; + _signal_value_changed.emit(get_property(_uris.ingen_value)); + } +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/ingen_client.cpp b/src/client/ingen_client.cpp new file mode 100644 index 00000000..fe9d6605 --- /dev/null +++ b/src/client/ingen_client.cpp @@ -0,0 +1,34 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Module.hpp" +#include "ingen/World.hpp" + +#include "ingen_config.h" + +struct IngenClientModule : public Ingen::Module { + void load(Ingen::World* world) {} +}; + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + return new IngenClientModule(); +} + +} // extern "C" diff --git a/src/client/wscript b/src/client/wscript new file mode 100644 index 00000000..df575c0d --- /dev/null +++ b/src/client/wscript @@ -0,0 +1,23 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf + +def build(bld): + obj = bld(features = 'cxx cxxshlib', + includes = ['../..'], + export_includes = ['../..'], + name = 'libingen_client', + target = 'ingen_client', + install_path = '${LIBDIR}', + use = 'libingen') + autowaf.use_lib(bld, obj, 'GLIBMM LV2 LILV SUIL RAUL SERD SORD SIGCPP') + + obj.source = ''' + BlockModel.cpp + ClientStore.cpp + GraphModel.cpp + ObjectModel.cpp + PluginModel.cpp + PluginUI.cpp + PortModel.cpp + ingen_client.cpp + ''' diff --git a/src/gui/App.cpp b/src/gui/App.cpp new file mode 100644 index 00000000..9f1a29ca --- /dev/null +++ b/src/gui/App.cpp @@ -0,0 +1,499 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <fstream> +#include <string> +#include <utility> + +#include <boost/variant/get.hpp> +#include <gtk/gtkwindow.h> +#include <gtkmm/stock.h> + +#include "ganv/Edge.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/QueuedInterface.hpp" +#include "ingen/StreamWriter.hpp" +#include "ingen/World.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/ObjectModel.hpp" +#include "ingen/client/SigClientInterface.hpp" +#include "ingen/runtime_paths.hpp" +#include "lilv/lilv.h" +#include "raul/Path.hpp" +#include "suil/suil.h" + +#include "App.hpp" +#include "ConnectWindow.hpp" +#include "GraphTreeWindow.hpp" +#include "GraphWindow.hpp" +#include "LoadPluginWindow.hpp" +#include "MessagesWindow.hpp" +#include "NodeModule.hpp" +#include "Port.hpp" +#include "RDFS.hpp" +#include "Style.hpp" +#include "SubgraphModule.hpp" +#include "ThreadedLoader.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" +#include "rgba.hpp" + +namespace Raul { class Deletable; } + +namespace Ingen { + +namespace Client { class PluginModel; } + +using namespace Client; + +namespace GUI { + +class Port; + +Gtk::Main* App::_main = nullptr; + +App::App(Ingen::World* world) + : _style(new Style(*this)) + , _about_dialog(nullptr) + , _window_factory(new WindowFactory(*this)) + , _world(world) + , _sample_rate(48000) + , _block_length(1024) + , _n_threads(1) + , _mean_run_load(0.0f) + , _min_run_load(0.0f) + , _max_run_load(0.0f) + , _enable_signal(true) + , _requested_plugins(false) + , _is_plugin(false) +{ + _world->conf().load_default("ingen", "gui.ttl"); + + WidgetFactory::get_widget_derived("connect_win", _connect_window); + WidgetFactory::get_widget_derived("messages_win", _messages_window); + WidgetFactory::get_widget_derived("graph_tree_win", _graph_tree_window); + WidgetFactory::get_widget("about_win", _about_dialog); + _connect_window->init_dialog(*this); + _messages_window->init_window(*this); + _graph_tree_window->init_window(*this); + _about_dialog->property_program_name() = "Ingen"; + _about_dialog->property_logo_icon_name() = "ingen"; + + PluginModel::set_rdf_world(*world->rdf_world()); + PluginModel::set_lilv_world(world->lilv_world()); + + using namespace std::placeholders; + world->log().set_sink(std::bind(&MessagesWindow::log, _messages_window, _1, _2, _3)); +} + +App::~App() +{ + delete _style; + delete _window_factory; +} + +SPtr<App> +App::create(Ingen::World* world) +{ + suil_init(&world->argc(), &world->argv(), SUIL_ARG_NONE); + + // Add RC file for embedded GUI Gtk style + const std::string rc_path = Ingen::data_file_path("ingen_style.rc"); + Gtk::RC::add_default_file(rc_path); + + _main = Gtk::Main::instance(); + if (!_main) { + Glib::set_application_name("Ingen"); + gtk_window_set_default_icon_name("ingen"); + _main = new Gtk::Main(&world->argc(), &world->argv()); + } + + App* app = new App(world); + + // Load configuration settings + app->style()->load_settings(); + app->style()->apply_settings(); + + // Set default window icon + app->_about_dialog->property_program_name() = "Ingen"; + app->_about_dialog->property_logo_icon_name() = "ingen"; + gtk_window_set_default_icon_name("ingen"); + + return SPtr<App>(app); +} + +void +App::run() +{ + _connect_window->start(*this, world()); + + // Run main iterations here until we're attached to the engine. Otherwise + // with 'ingen -egl' we'd get a bunch of notifications about load + // immediately before even knowing about the root graph or plugins) + while (!_connect_window->attached()) { + if (_main->iteration()) { + break; + } + } + + _main->run(); +} + +void +App::attach(SPtr<Ingen::Interface> client) +{ + assert(!_client); + assert(!_store); + assert(!_loader); + + if (_world->engine()) { + _world->engine()->register_client(client); + } + + _client = client; + _store = SPtr<ClientStore>(new ClientStore(_world->uris(), _world->log(), sig_client())); + _loader = SPtr<ThreadedLoader>(new ThreadedLoader(*this, _world->interface())); + if (!_world->store()) { + _world->set_store(_store); + } + + if (_world->conf().option("dump").get<int32_t>()) { + _dumper = SPtr<StreamWriter>(new StreamWriter(_world->uri_map(), + _world->uris(), + URI("ingen:/client"), + stderr, + ColorContext::Color::CYAN)); + + sig_client()->signal_message().connect( + sigc::mem_fun(*_dumper.get(), &StreamWriter::message)); + } + + _graph_tree_window->init(*this, *_store); + sig_client()->signal_message().connect(sigc::mem_fun(this, &App::message)); +} + +void +App::detach() +{ + if (_world->interface()) { + _window_factory->clear(); + _store->clear(); + + _loader.reset(); + _store.reset(); + _client.reset(); + _world->set_interface(SPtr<Interface>()); + } +} + +void +App::request_plugins_if_necessary() +{ + if (!_requested_plugins) { + _world->interface()->get(URI("ingen:/plugins")); + _requested_plugins = true; + } +} + +SPtr<SigClientInterface> +App::sig_client() +{ + SPtr<QueuedInterface> qi = dynamic_ptr_cast<QueuedInterface>(_client); + if (qi) { + return dynamic_ptr_cast<SigClientInterface>(qi->sink()); + } + return dynamic_ptr_cast<SigClientInterface>(_client); +} + +SPtr<Serialiser> +App::serialiser() +{ + return _world->serialiser(); +} + +void +App::message(const Message& msg) +{ + if (const Response* const r = boost::get<Response>(&msg)) { + response(r->id, r->status, r->subject); + } else if (const Error* const e = boost::get<Error>(&msg)) { + error_message(e->message); + } else if (const Put* const p = boost::get<Put>(&msg)) { + put(p->uri, p->properties, p->ctx); + } else if (const SetProperty* const s = boost::get<SetProperty>(&msg)) { + property_change(s->subject, s->predicate, s->value, s->ctx); + } +} + +void +App::response(int32_t id, Status status, const std::string& subject) +{ + if (status != Status::SUCCESS) { + std::string msg = ingen_status_string(status); + if (!subject.empty()) { + msg += ": " + subject; + } + error_message(msg); + } +} + +void +App::error_message(const std::string& str) +{ + _messages_window->post_error(str); +} + +void +App::set_property(const URI& subject, + const URI& key, + const Atom& value, + Resource::Graph ctx) +{ + // Send message to server + interface()->set_property(subject, key, value, ctx); + + /* The server does not feed back set messages (kludge to prevent control + feedback and bandwidth wastage, see Delta.cpp). So, assume everything + went as planned here and fire the signal ourselves as if the server + feedback came back immediately. */ + if (key != uris().ingen_activity) { + sig_client()->signal_message().emit(SetProperty{0, subject, key, value, ctx}); + } +} + +void +App::set_tooltip(Gtk::Widget* widget, const LilvNode* node) +{ + const std::string comment = RDFS::comment(_world, node); + if (!comment.empty()) { + widget->set_tooltip_text(comment); + } +} + +void +App::put(const URI& uri, + const Properties& properties, + Resource::Graph ctx) +{ + _enable_signal = false; + for (const auto& p : properties) { + property_change(uri, p.first, p.second); + } + _enable_signal = true; + _status_text = status_text(); + signal_status_text_changed.emit(_status_text); +} + +void +App::property_change(const URI& subject, + const URI& key, + const Atom& value, + Resource::Graph ctx) +{ + if (subject != URI("ingen:/engine")) { + return; + } else if (key == uris().param_sampleRate && value.type() == forge().Int) { + _sample_rate = value.get<int32_t>(); + } else if (key == uris().bufsz_maxBlockLength && value.type() == forge().Int) { + _block_length = value.get<int32_t>(); + } else if (key == uris().ingen_numThreads && value.type() == forge().Int) { + _n_threads = value.get<int>(); + } else if (key == uris().ingen_minRunLoad && value.type() == forge().Float) { + _min_run_load = value.get<float>(); + } else if (key == uris().ingen_meanRunLoad && value.type() == forge().Float) { + _mean_run_load = value.get<float>(); + } else if (key == uris().ingen_maxRunLoad && value.type() == forge().Float) { + _max_run_load = value.get<float>(); + } else { + _world->log().warn(fmt("Unknown engine property %1%\n") % key); + return; + } + + if (_enable_signal) { + _status_text = status_text(); + signal_status_text_changed.emit(_status_text); + } +} + +static std::string +fraction_label(float f) +{ + static const uint32_t GREEN = 0x4A8A0EFF; + static const uint32_t RED = 0x960909FF; + + const uint32_t col = rgba_interpolate(GREEN, RED, std::min(f, 1.0f)); + char col_str[8]; + snprintf(col_str, sizeof(col_str), "%02X%02X%02X", + RGBA_R(col), RGBA_G(col), RGBA_B(col)); + return (fmt("<span color='#%s'>%d%%</span>") % col_str % (f * 100)).str(); +} + +std::string +App::status_text() const +{ + return (fmt("%2.1f kHz / %.1f ms, %s, %s DSP") + % (_sample_rate / 1e3f) + % (_block_length * 1e3f / (float)_sample_rate) + % ((_n_threads == 1) + ? "1 thread" + : (fmt("%1% threads") % _n_threads).str()) + % fraction_label(_max_run_load)).str(); +} + +void +App::port_activity(Port* port) +{ + std::pair<ActivityPorts::iterator, bool> inserted = _activity_ports.emplace(port, false); + if (inserted.second) { + inserted.first->second = false; + } + + port->set_highlighted(true); +} + +void +App::activity_port_destroyed(Port* port) +{ + auto i = _activity_ports.find(port); + if (i != _activity_ports.end()) { + _activity_ports.erase(i); + } +} + +bool +App::animate() +{ + for (auto i = _activity_ports.begin(); i != _activity_ports.end(); ) { + auto next = i; + ++next; + + if ((*i).second) { // saw it last time, unhighlight and pop + (*i).first->set_highlighted(false); + _activity_ports.erase(i); + } else { + (*i).second = true; + } + + i = next; + } + + return true; +} + +/******** Event Handlers ************/ + +void +App::register_callbacks() +{ + Glib::signal_timeout().connect( + sigc::mem_fun(*this, &App::gtk_main_iteration), 33, G_PRIORITY_DEFAULT); +} + +bool +App::gtk_main_iteration() +{ + if (!_client) { + return false; + } + + animate(); + + if (_messages_window) { + _messages_window->flush(); + } + + _enable_signal = false; + if (_world->engine()) { + if (!_world->engine()->main_iteration()) { + Gtk::Main::quit(); + return false; + } + } else { + dynamic_ptr_cast<QueuedInterface>(_client)->emit(); + } + _enable_signal = true; + + return true; +} + +void +App::show_about() +{ + _about_dialog->run(); + _about_dialog->hide(); +} + +/** Prompt (if necessary) and quit application (if confirmed). + * @return true iff the application quit. + */ +bool +App::quit(Gtk::Window* dialog_parent) +{ + bool quit = true; + if (_world->engine() && _connect_window->attached()) { + Gtk::MessageDialog d( + "The engine is running in this process. Quitting will terminate Ingen." + "\n\n" "Are you sure you want to quit?", + true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE, true); + if (dialog_parent) { + d.set_transient_for(*dialog_parent); + } + d.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + d.add_button(Gtk::Stock::QUIT, Gtk::RESPONSE_CLOSE); + quit = (d.run() == Gtk::RESPONSE_CLOSE); + } + + if (!quit) { + return false; + } + + Gtk::Main::quit(); + + try { + const std::string path = _world->conf().save( + _world->uri_map(), "ingen", "gui.ttl", Configuration::GUI); + std::cout << (fmt("Saved GUI settings to %1%\n") % path); + } catch (const std::exception& e) { + std::cerr << (fmt("Error saving GUI settings (%1%)\n") + % e.what()); + } + + return true; +} + +bool +App::can_control(const Client::PortModel* port) const +{ + return port->is_a(uris().lv2_ControlPort) + || port->is_a(uris().lv2_CVPort) + || (port->is_a(uris().atom_AtomPort) + && (port->supports(uris().atom_Float) + || port->supports(uris().atom_String))); +} + +uint32_t +App::sample_rate() const +{ + return _sample_rate; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/App.hpp b/src/gui/App.hpp new file mode 100644 index 00000000..75661449 --- /dev/null +++ b/src/gui/App.hpp @@ -0,0 +1,196 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_APP_HPP +#define INGEN_GUI_APP_HPP + +#include <unordered_map> +#include <string> + +#include <gtkmm/aboutdialog.h> +#include <gtkmm/main.h> +#include <gtkmm/window.h> + +#include "ingen/Atom.hpp" +#include "ingen/Message.hpp" +#include "ingen/Resource.hpp" +#include "ingen/Status.hpp" +#include "ingen/World.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "lilv/lilv.h" +#include "raul/Deletable.hpp" + +namespace Ingen { + +class Interface; +class Log; +class Port; +class Serialiser; +class StreamWriter; +class World; + +namespace Client { + +class ClientStore; +class GraphModel; +class PluginModel; +class PortModel; +class SigClientInterface; + +} + +namespace GUI { + +class ConnectWindow; +class GraphCanvas; +class GraphTreeView; +class GraphTreeWindow; +class MessagesWindow; +class Port; +class Style; +class ThreadedLoader; +class WindowFactory; + +/** Ingen Gtk Application. + * \ingroup GUI + */ +class INGEN_API App +{ +public: + ~App(); + + void error_message(const std::string& str); + + void attach(SPtr<Ingen::Interface> client); + + void detach(); + + void request_plugins_if_necessary(); + + void register_callbacks(); + bool gtk_main_iteration(); + + void show_about(); + bool quit(Gtk::Window* dialog_parent); + + void port_activity(Port* port); + void activity_port_destroyed(Port* port); + bool can_control(const Client::PortModel* port) const; + + bool signal() const { return _enable_signal; } + void enable_signals(bool b) { _enable_signal = b; } + bool disable_signals() { + bool old = _enable_signal; + _enable_signal = false; + return old; + } + + void set_property(const URI& subject, + const URI& key, + const Atom& value, + Resource::Graph ctx = Resource::Graph::DEFAULT); + + /** Set the tooltip for a widget from its RDF documentation. */ + void set_tooltip(Gtk::Widget* widget, const LilvNode* node); + + uint32_t sample_rate() const; + + bool is_plugin() const { return _is_plugin; } + void set_is_plugin(bool b) { _is_plugin = b; } + + ConnectWindow* connect_window() const { return _connect_window; } + MessagesWindow* messages_dialog() const { return _messages_window; } + GraphTreeWindow* graph_tree() const { return _graph_tree_window; } + Style* style() const { return _style; } + WindowFactory* window_factory() const { return _window_factory; } + + Ingen::Forge& forge() const { return _world->forge(); } + SPtr<Ingen::Interface> interface() const { return _world->interface(); } + SPtr<Ingen::Interface> client() const { return _client; } + SPtr<Client::ClientStore> store() const { return _store; } + SPtr<ThreadedLoader> loader() const { return _loader; } + + SPtr<Client::SigClientInterface> sig_client(); + + SPtr<Serialiser> serialiser(); + + static SPtr<App> create(Ingen::World* world); + + void run(); + + std::string status_text() const; + + sigc::signal<void, const std::string&> signal_status_text_changed; + + inline Ingen::World* world() const { return _world; } + inline Ingen::URIs& uris() const { return _world->uris(); } + inline Ingen::Log& log() const { return _world->log(); } + +protected: + explicit App(Ingen::World* world); + + void message(const Ingen::Message& msg); + + bool animate(); + void response(int32_t id, Ingen::Status status, const std::string& subject); + + void put(const URI& uri, + const Properties& properties, + Resource::Graph ctx); + + void property_change(const URI& subject, + const URI& key, + const Atom& value, + Resource::Graph ctx = Resource::Graph::DEFAULT); + + static Gtk::Main* _main; + + SPtr<Ingen::Interface> _client; + SPtr<Client::ClientStore> _store; + SPtr<ThreadedLoader> _loader; + SPtr<StreamWriter> _dumper; + + Style* _style; + + ConnectWindow* _connect_window; + MessagesWindow* _messages_window; + GraphTreeWindow* _graph_tree_window; + Gtk::AboutDialog* _about_dialog; + WindowFactory* _window_factory; + + Ingen::World* _world; + + int32_t _sample_rate; + int32_t _block_length; + int32_t _n_threads; + float _mean_run_load; + float _min_run_load; + float _max_run_load; + std::string _status_text; + + typedef std::unordered_map<Port*, bool> ActivityPorts; + ActivityPorts _activity_ports; + + bool _enable_signal; + bool _requested_plugins; + bool _is_plugin; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_APP_HPP diff --git a/src/gui/Arc.cpp b/src/gui/Arc.cpp new file mode 100644 index 00000000..c14b2e88 --- /dev/null +++ b/src/gui/Arc.cpp @@ -0,0 +1,44 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "Arc.hpp" +#include "ingen/client/ArcModel.hpp" +#include "ingen/client/BlockModel.hpp" + +#define NS_INTERNALS "http://drobilla.net/ns/ingen-internals#" + +namespace Ingen { +namespace GUI { + +Arc::Arc(Ganv::Canvas& canvas, + SPtr<const Client::ArcModel> model, + Ganv::Node* src, + Ganv::Node* dst) + : Ganv::Edge(canvas, src, dst) + , _arc_model(model) +{ + SPtr<const Client::ObjectModel> tparent = model->tail()->parent(); + SPtr<const Client::BlockModel> tparent_block; + if ((tparent_block = dynamic_ptr_cast<const Client::BlockModel>(tparent))) { + if (tparent_block->plugin_uri() == NS_INTERNALS "BlockDelay") { + g_object_set(_gobj, "dash-length", 4.0, NULL); + set_constraining(false); + } + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/Arc.hpp b/src/gui/Arc.hpp new file mode 100644 index 00000000..382ca305 --- /dev/null +++ b/src/gui/Arc.hpp @@ -0,0 +1,52 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_ARC_HPP +#define INGEN_GUI_ARC_HPP + +#include <cassert> + +#include "ganv/Edge.hpp" +#include "ingen/types.hpp" + +namespace Ingen { + +namespace Client { class ArcModel; } + +namespace GUI { + +/** An Arc (directed edge) in a Graph. + * + * \ingroup GUI + */ +class Arc : public Ganv::Edge +{ +public: + Arc(Ganv::Canvas& canvas, + SPtr<const Client::ArcModel> model, + Ganv::Node* src, + Ganv::Node* dst); + + SPtr<const Client::ArcModel> model() const { return _arc_model; } + +private: + SPtr<const Client::ArcModel> _arc_model; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_ARC_HPP diff --git a/src/gui/BreadCrumbs.cpp b/src/gui/BreadCrumbs.cpp new file mode 100644 index 00000000..ae7882e3 --- /dev/null +++ b/src/gui/BreadCrumbs.cpp @@ -0,0 +1,229 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <list> +#include <string> + +#include <boost/variant/get.hpp> + +#include "ingen/client/SigClientInterface.hpp" + +#include "App.hpp" +#include "BreadCrumbs.hpp" + +namespace Ingen { +namespace GUI { + +using std::string; + +BreadCrumbs::BreadCrumbs(App& app) + : Gtk::HBox() + , _active_path("/") + , _full_path("/") + , _enable_signal(true) +{ + app.sig_client()->signal_message().connect( + sigc::mem_fun(this, &BreadCrumbs::message)); + + set_can_focus(false); +} + +SPtr<GraphView> +BreadCrumbs::view(const Raul::Path& path) +{ + for (const auto& b : _breadcrumbs) { + if (b->path() == path) { + return b->view(); + } + } + + return SPtr<GraphView>(); +} + +/** Sets up the crumbs to display `path`. + * + * If `path` is already part of the shown path, it will be selected and the + * children preserved. + */ +void +BreadCrumbs::build(Raul::Path path, SPtr<GraphView> view) +{ + bool old_enable_signal = _enable_signal; + _enable_signal = false; + + if (!_breadcrumbs.empty() && (path.is_parent_of(_full_path) || path == _full_path)) { + // Moving to a path we already contain, just switch the active button + for (const auto& b : _breadcrumbs) { + if (b->path() == path) { + b->set_active(true); + if (!b->view()) { + b->set_view(view); + } + + // views are expensive, having two around for the same graph is a bug + assert(b->view() == view); + + } else { + b->set_active(false); + } + } + + _active_path = path; + _enable_signal = old_enable_signal; + + } else if (!_breadcrumbs.empty() && path.is_child_of(_full_path)) { + // Moving to a child of the full path, just append crumbs (preserve view cache) + + string suffix = path.substr(_full_path.length()); + while (suffix.length() > 0) { + if (suffix[0] == '/') { + suffix = suffix.substr(1); + } + const string name = suffix.substr(0, suffix.find("/")); + _full_path = _full_path.child(Raul::Symbol(name)); + BreadCrumb* but = create_crumb(_full_path, view); + pack_start(*but, false, false, 1); + _breadcrumbs.push_back(but); + but->show(); + if (suffix.find("/") == string::npos) { + break; + } else { + suffix = suffix.substr(suffix.find("/")+1); + } + } + + for (const auto& b : _breadcrumbs) { + b->set_active(false); + } + _breadcrumbs.back()->set_active(true); + + } else { + // Rebuild from scratch + // Getting here is bad unless absolutely necessary, since the GraphView cache is lost + + _full_path = path; + _active_path = path; + + // Empty existing breadcrumbs + for (const auto& b : _breadcrumbs) { + remove(*b); + } + _breadcrumbs.clear(); + + // Add root + BreadCrumb* root_but = create_crumb(Raul::Path("/"), view); + pack_start(*root_but, false, false, 1); + _breadcrumbs.push_front(root_but); + root_but->set_active(root_but->path() == _active_path); + + Raul::Path working_path("/"); + string suffix = path.substr(1); + while (suffix.length() > 0) { + if (suffix[0] == '/') { + suffix = suffix.substr(1); + } + const string name = suffix.substr(0, suffix.find("/")); + working_path = working_path.child(Raul::Symbol(name)); + BreadCrumb* but = create_crumb(working_path, view); + pack_start(*but, false, false, 1); + _breadcrumbs.push_back(but); + but->set_active(working_path == _active_path); + but->show(); + if (suffix.find("/") == string::npos) { + break; + } else { + suffix = suffix.substr(suffix.find("/")+1); + } + } + } + + _enable_signal = old_enable_signal; +} + +/** Create a new crumb, assigning it a reference to `view` if their paths + * match, otherwise ignoring `view`. + */ +BreadCrumbs::BreadCrumb* +BreadCrumbs::create_crumb(const Raul::Path& path, + SPtr<GraphView> view) +{ + BreadCrumb* but = manage( + new BreadCrumb(path, + ((view && path == view->graph()->path()) + ? view : SPtr<GraphView>()))); + + but->signal_toggled().connect( + sigc::bind(sigc::mem_fun(this, &BreadCrumbs::breadcrumb_clicked), + but)); + + return but; +} + +void +BreadCrumbs::breadcrumb_clicked(BreadCrumb* crumb) +{ + if (_enable_signal) { + _enable_signal = false; + + if (!crumb->get_active()) { + // Tried to turn off the current active button, bad user, no cookie + crumb->set_active(true); + } else { + signal_graph_selected.emit(crumb->path(), crumb->view()); + if (crumb->path() != _active_path) { + crumb->set_active(false); + } + } + _enable_signal = true; + } +} + +void +BreadCrumbs::message(const Message& msg) +{ + if (const Del* const del = boost::get<Del>(&msg)) { + object_destroyed(del->uri); + } +} + +void +BreadCrumbs::object_destroyed(const URI& uri) +{ + for (auto i = _breadcrumbs.begin(); i != _breadcrumbs.end(); ++i) { + if ((*i)->path() == uri.c_str()) { + // Remove all crumbs after the removed one (inclusive) + for (auto j = i; j != _breadcrumbs.end(); ) { + BreadCrumb* bc = *j; + j = _breadcrumbs.erase(j); + remove(*bc); + } + break; + } + } +} + +void +BreadCrumbs::object_moved(const Raul::Path& old_path, const Raul::Path& new_path) +{ + for (const auto& b : _breadcrumbs) { + if (b->path() == old_path) { + b->set_path(new_path); + } + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/BreadCrumbs.hpp b/src/gui/BreadCrumbs.hpp new file mode 100644 index 00000000..467d3bfc --- /dev/null +++ b/src/gui/BreadCrumbs.hpp @@ -0,0 +1,119 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_BREADCRUMBS_HPP +#define INGEN_GUI_BREADCRUMBS_HPP + +#include <list> + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/togglebutton.h> + +#include "raul/Path.hpp" + +#include "ingen/client/GraphModel.hpp" +#include "ingen/types.hpp" + +#include "GraphView.hpp" + +namespace Ingen { +namespace GUI { + +/** Collection of breadcrumb buttons forming a path. + * This doubles as a cache for GraphViews. + * + * \ingroup GUI + */ +class BreadCrumbs : public Gtk::HBox +{ +public: + explicit BreadCrumbs(App& app); + + SPtr<GraphView> view(const Raul::Path& path); + + void build(Raul::Path path, SPtr<GraphView> view); + + sigc::signal<void, const Raul::Path&, SPtr<GraphView> > signal_graph_selected; + +private: + /** Breadcrumb button. + * + * Each Breadcrumb stores a reference to a GraphView for quick switching. + * So, the amount of allocated GraphViews at a given time is equal to the + * number of visible breadcrumbs (which is the perfect cache for GUI + * responsiveness balanced with mem consumption). + * + * \ingroup GUI + */ + class BreadCrumb : public Gtk::ToggleButton + { + public: + BreadCrumb(const Raul::Path& path, SPtr<GraphView> view = SPtr<GraphView>()) + : _path(path) + , _view(view) + { + assert(!view || view->graph()->path() == path); + set_border_width(0); + set_path(path); + set_can_focus(false); + show_all(); + } + + void set_view(SPtr<GraphView> view) { + assert(!view || view->graph()->path() == _path); + _view = view; + } + + const Raul::Path& path() const { return _path; } + SPtr<GraphView> view() const { return _view; } + + void set_path(const Raul::Path& path) { + remove(); + const char* text = (path.is_root()) ? "/" : path.symbol(); + Gtk::Label* lab = manage(new Gtk::Label(text)); + lab->set_padding(0, 0); + lab->show(); + add(*lab); + + if (_view && _view->graph()->path() != path) + _view.reset(); + } + + private: + Raul::Path _path; + SPtr<GraphView> _view; + }; + + BreadCrumb* create_crumb(const Raul::Path& path, + SPtr<GraphView> view = SPtr<GraphView>()); + + void breadcrumb_clicked(BreadCrumb* crumb); + + void message(const Message& msg); + void object_destroyed(const URI& uri); + void object_moved(const Raul::Path& old_path, const Raul::Path& new_path); + + Raul::Path _active_path; + Raul::Path _full_path; + bool _enable_signal; + std::list<BreadCrumb*> _breadcrumbs; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_BREADCRUMBS_HPP diff --git a/src/gui/ConnectWindow.cpp b/src/gui/ConnectWindow.cpp new file mode 100644 index 00000000..458a43dd --- /dev/null +++ b/src/gui/ConnectWindow.cpp @@ -0,0 +1,572 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <limits> +#include <sstream> +#include <string> + +#include <boost/variant/get.hpp> +#include <gtkmm/stock.h> + +#include "raul/Process.hpp" + +#include "ingen/Configuration.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/QueuedInterface.hpp" +#include "ingen/World.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/SigClientInterface.hpp" +#include "ingen/client/SocketClient.hpp" +#include "ingen_config.h" + +#include "App.hpp" +#include "ConnectWindow.hpp" +#include "WindowFactory.hpp" + +using namespace Ingen::Client; + +namespace Ingen { +namespace GUI { + +ConnectWindow::ConnectWindow(BaseObjectType* cobject, + Glib::RefPtr<Gtk::Builder> xml) + : Dialog(cobject) + , _xml(std::move(xml)) + , _icon(nullptr) + , _progress_bar(nullptr) + , _progress_label(nullptr) + , _url_entry(nullptr) + , _server_radio(nullptr) + , _port_spinbutton(nullptr) + , _launch_radio(nullptr) + , _internal_radio(nullptr) + , _activate_button(nullptr) + , _deactivate_button(nullptr) + , _disconnect_button(nullptr) + , _connect_button(nullptr) + , _quit_button(nullptr) + , _mode(Mode::CONNECT_REMOTE) + , _connect_uri("unix:///tmp/ingen.sock") + , _ping_id(-1) + , _attached(false) + , _finished_connecting(false) + , _widgets_loaded(false) + , _connect_stage(0) + , _quit_flag(false) +{ +} + +void +ConnectWindow::message(const Message& msg) +{ + if (const Response* const r = boost::get<Response>(&msg)) { + ingen_response(r->id, r->status, r->subject); + } else if (const Error* const e = boost::get<Error>(&msg)) { + error(e->message); + } +} + +void +ConnectWindow::error(const std::string& msg) +{ + if (!is_visible()) { + present(); + set_connecting_widget_states(); + } + + if (_progress_label) { + _progress_label->set_text(msg); + } + + if (_app && _app->world()) { + _app->world()->log().error(msg + "\n"); + } +} + +void +ConnectWindow::start(App& app, Ingen::World* world) +{ + _app = &app; + + if (world->engine()) { + _mode = Mode::INTERNAL; + } + + set_connected_to(world->interface()); + connect(bool(world->interface())); +} + +void +ConnectWindow::ingen_response(int32_t id, + Status status, + const std::string& subject) +{ + if (id == _ping_id) { + if (status != Status::SUCCESS) { + error("Failed to get root patch"); + } else { + _attached = true; + } + } +} + +void +ConnectWindow::set_connected_to(SPtr<Ingen::Interface> engine) +{ + _app->world()->set_interface(engine); + + if (!_widgets_loaded) { + return; + } + + if (engine) { + _icon->set(Gtk::Stock::CONNECT, Gtk::ICON_SIZE_LARGE_TOOLBAR); + _progress_bar->set_fraction(1.0); + _progress_label->set_text("Connected to engine"); + _url_entry->set_sensitive(false); + _url_entry->set_text(engine->uri().string()); + _connect_button->set_sensitive(false); + _disconnect_button->set_label("gtk-disconnect"); + _disconnect_button->set_sensitive(true); + _port_spinbutton->set_sensitive(false); + _launch_radio->set_sensitive(false); + _internal_radio->set_sensitive(false); + _activate_button->set_sensitive(true); + _deactivate_button->set_sensitive(true); + + } else { + _icon->set(Gtk::Stock::DISCONNECT, Gtk::ICON_SIZE_LARGE_TOOLBAR); + _progress_bar->set_fraction(0.0); + _connect_button->set_sensitive(true); + _disconnect_button->set_sensitive(false); + _internal_radio->set_sensitive(true); + _server_radio->set_sensitive(true); + _launch_radio->set_sensitive(true); + _activate_button->set_sensitive(false); + _deactivate_button->set_sensitive(false); + + if (_mode == Mode::CONNECT_REMOTE) { + _url_entry->set_sensitive(true); + } else if (_mode == Mode::LAUNCH_REMOTE) { + _port_spinbutton->set_sensitive(true); + } + + _progress_label->set_text(std::string("Disconnected")); + } +} + +void +ConnectWindow::set_connecting_widget_states() +{ + if (!_widgets_loaded) { + return; + } + + _connect_button->set_sensitive(false); + _disconnect_button->set_label("gtk-cancel"); + _disconnect_button->set_sensitive(true); + _server_radio->set_sensitive(false); + _launch_radio->set_sensitive(false); + _internal_radio->set_sensitive(false); + _url_entry->set_sensitive(false); + _port_spinbutton->set_sensitive(false); +} + +bool +ConnectWindow::connect_remote(const URI& uri) +{ + Ingen::World* world = _app->world(); + + SPtr<SigClientInterface> sci(new SigClientInterface()); + SPtr<QueuedInterface> qi(new QueuedInterface(sci)); + + SPtr<Ingen::Interface> iface(world->new_interface(uri, qi)); + if (iface) { + world->set_interface(iface); + _app->attach(qi); + _app->register_callbacks(); + return true; + } + + return false; +} + +/** Set up initial connect stage and launch connect callback. */ +void +ConnectWindow::connect(bool existing) +{ + if (_app->client()) { + error("Already connected"); + return; + } else if (_attached) { + _attached = false; + } + + set_connecting_widget_states(); + _connect_stage = 0; + + Ingen::World* world = _app->world(); + + if (_mode == Mode::CONNECT_REMOTE) { + std::string uri_str = world->conf().option("connect").ptr<char>(); + if (existing) { + uri_str = world->interface()->uri(); + _connect_stage = 1; + SPtr<Client::SocketClient> client = dynamic_ptr_cast<Client::SocketClient>(world->interface()); + if (client) { + _app->attach(client->respondee()); + _app->register_callbacks(); + } else { + error("Connected with invalid client interface type"); + return; + } + } else if (_widgets_loaded) { + uri_str = _url_entry->get_text(); + } + + if (!URI::is_valid(uri_str)) { + error((fmt("Invalid socket URI %1%") % uri_str).str()); + return; + } + + _connect_uri = URI(uri_str); + + } else if (_mode == Mode::LAUNCH_REMOTE) { + const std::string port = std::to_string(_port_spinbutton->get_value_as_int()); + const char* cmd[] = { "ingen", "-e", "-E", port.c_str(), nullptr }; + + if (!Raul::Process::launch(cmd)) { + error("Failed to launch engine process"); + return; + } + + _connect_uri = URI(std::string("tcp://localhost:") + port); + + } else if (_mode == Mode::INTERNAL) { + if (!world->engine()) { + if (!world->load_module("server")) { + error("Failed to load server module"); + return; + } else if (!world->load_module("jack")) { + error("Failed to load jack module"); + return; + } else if (!world->engine()->activate()) { + error("Failed to activate engine"); + return; + } + } + } + + set_connecting_widget_states(); + if (_widgets_loaded) { + _progress_label->set_text("Connecting..."); + } + Glib::signal_timeout().connect( + sigc::mem_fun(this, &ConnectWindow::gtk_callback), 33); +} + +void +ConnectWindow::disconnect() +{ + _connect_stage = -1; + _attached = false; + + _app->detach(); + set_connected_to(SPtr<Ingen::Interface>()); + + if (!_widgets_loaded) { + return; + } + + _activate_button->set_sensitive(false); + _deactivate_button->set_sensitive(false); + + _progress_bar->set_fraction(0.0); + _connect_button->set_sensitive(true); + _disconnect_button->set_sensitive(false); +} + +void +ConnectWindow::activate() +{ + if (!_app->interface()) { + return; + } + + _app->interface()->set_property(URI("ingen:/driver"), + _app->uris().ingen_enabled, + _app->forge().make(true)); +} + +void +ConnectWindow::deactivate() +{ + if (!_app->interface()) { + return; + } + + _app->interface()->set_property(URI("ingen:/driver"), + _app->uris().ingen_enabled, + _app->forge().make(false)); +} + +void +ConnectWindow::on_show() +{ + if (!_widgets_loaded) { + load_widgets(); + } + + if (_attached) { + set_connected_to(_app->interface()); + } + + Gtk::Dialog::on_show(); +} + +void +ConnectWindow::load_widgets() +{ + _xml->get_widget("connect_icon", _icon); + _xml->get_widget("connect_progress_bar", _progress_bar); + _xml->get_widget("connect_progress_label", _progress_label); + _xml->get_widget("connect_server_radiobutton", _server_radio); + _xml->get_widget("connect_url_entry", _url_entry); + _xml->get_widget("connect_launch_radiobutton", _launch_radio); + _xml->get_widget("connect_port_spinbutton", _port_spinbutton); + _xml->get_widget("connect_internal_radiobutton", _internal_radio); + _xml->get_widget("connect_activate_button", _activate_button); + _xml->get_widget("connect_deactivate_button", _deactivate_button); + _xml->get_widget("connect_disconnect_button", _disconnect_button); + _xml->get_widget("connect_connect_button", _connect_button); + _xml->get_widget("connect_quit_button", _quit_button); + + _server_radio->signal_toggled().connect( + sigc::mem_fun(this, &ConnectWindow::server_toggled)); + _launch_radio->signal_toggled().connect( + sigc::mem_fun(this, &ConnectWindow::launch_toggled)); + _internal_radio->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::internal_toggled)); + _activate_button->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::activate)); + _deactivate_button->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::deactivate)); + _disconnect_button->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::disconnect)); + _connect_button->signal_clicked().connect( + sigc::bind(sigc::mem_fun(this, &ConnectWindow::connect), false)); + _quit_button->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::quit_clicked)); + + _url_entry->set_text(_app->world()->conf().option("connect").ptr<char>()); + if (URI::is_valid(_url_entry->get_text())) { + _connect_uri = URI(_url_entry->get_text()); + } + + _port_spinbutton->set_range(1, std::numeric_limits<uint16_t>::max()); + _port_spinbutton->set_increments(1, 100); + _port_spinbutton->set_value( + _app->world()->conf().option("engine-port").get<int32_t>()); + + _progress_bar->set_pulse_step(0.01); + _widgets_loaded = true; + + server_toggled(); +} + +void +ConnectWindow::on_hide() +{ + Gtk::Dialog::on_hide(); + if (_app->window_factory()->num_open_graph_windows() == 0) { + quit(); + } +} + +void +ConnectWindow::quit_clicked() +{ + if (_app->quit(this)) { + _quit_flag = true; + } +} + +void +ConnectWindow::server_toggled() +{ + _url_entry->set_sensitive(true); + _port_spinbutton->set_sensitive(false); + _mode = Mode::CONNECT_REMOTE; +} + +void +ConnectWindow::launch_toggled() +{ + _url_entry->set_sensitive(false); + _port_spinbutton->set_sensitive(true); + _mode = Mode::LAUNCH_REMOTE; +} + +void +ConnectWindow::internal_toggled() +{ + _url_entry->set_sensitive(false); + _port_spinbutton->set_sensitive(false); + _mode = Mode::INTERNAL; +} + +void +ConnectWindow::next_stage() +{ + static const char* labels[] = { + "Connecting...", + "Pinging engine...", + "Attaching to engine...", + "Requesting root graph...", + "Loading plugins...", + "Connected" + }; + + + ++_connect_stage; + if (_widgets_loaded) { + _progress_label->set_text(labels[_connect_stage]); + } +} + +bool +ConnectWindow::gtk_callback() +{ + /* If I call this a "state machine" it's not ugly code any more */ + + if (_quit_flag) { + return false; // deregister this callback + } + + // Timing stuff for repeated attach attempts + timeval now; + gettimeofday(&now, nullptr); + static const timeval start = now; + static timeval last = now; + static unsigned attempts = 0; + + // Show if attempted connection goes on for a noticeable amount of time + if (!is_visible()) { + const float ms_since_start = (now.tv_sec - start.tv_sec) * 1000.0f + + (now.tv_usec - start.tv_usec) * 0.001f; + if (ms_since_start > 500) { + present(); + set_connecting_widget_states(); + } + } + + if (_connect_stage == 0) { + const float ms_since_last = (now.tv_sec - last.tv_sec) * 1000.0f + + (now.tv_usec - last.tv_usec) * 0.001f; + if (ms_since_last >= 250) { + last = now; + if (_mode == Mode::INTERNAL) { + SPtr<SigClientInterface> client(new SigClientInterface()); + _app->world()->interface()->set_respondee(client); + _app->attach(client); + _app->register_callbacks(); + next_stage(); + } else if (connect_remote(_connect_uri)) { + next_stage(); + } + } + } else if (_connect_stage == 1) { + _attached = false; + _app->sig_client()->signal_message().connect( + sigc::mem_fun(this, &ConnectWindow::message)); + + _ping_id = g_random_int_range(1, std::numeric_limits<int32_t>::max()); + _app->interface()->set_response_id(_ping_id); + _app->interface()->get(URI("ingen:/engine")); + last = now; + attempts = 0; + next_stage(); + + } else if (_connect_stage == 2) { + if (_attached) { + next_stage(); + } else { + const float ms_since_last = (now.tv_sec - last.tv_sec) * 1000.0f + + (now.tv_usec - last.tv_usec) * 0.001f; + if (attempts > 10) { + error("Failed to ping engine"); + _connect_stage = -1; + } else if (ms_since_last > 1000) { + _app->interface()->set_response_id(_ping_id); + _app->interface()->get(URI("ingen:/engine")); + last = now; + ++attempts; + } + } + } else if (_connect_stage == 3) { + _app->interface()->get(URI(main_uri().string() + "/")); + next_stage(); + } else if (_connect_stage == 4) { + if (_app->store()->size() > 0) { + SPtr<const GraphModel> root = dynamic_ptr_cast<const GraphModel>( + _app->store()->object(Raul::Path("/"))); + if (root) { + set_connected_to(_app->interface()); + _app->window_factory()->present_graph(root); + next_stage(); + } + } + } else if (_connect_stage == 5) { + hide(); + _connect_stage = 0; // set ourselves up for next time (if there is one) + _finished_connecting = true; + _app->interface()->set_response_id(1); + return false; // deregister this callback + } + + if (_widgets_loaded) { + _progress_bar->pulse(); + } + + if (_connect_stage == -1) { // we were cancelled + if (_widgets_loaded) { + _icon->set(Gtk::Stock::DISCONNECT, Gtk::ICON_SIZE_LARGE_TOOLBAR); + _progress_bar->set_fraction(0.0); + _connect_button->set_sensitive(true); + _disconnect_button->set_sensitive(false); + _disconnect_button->set_label("gtk-disconnect"); + _progress_label->set_text(std::string("Disconnected")); + } + return false; + } else { + return true; + } +} + +void +ConnectWindow::quit() +{ + _quit_flag = true; + Gtk::Main::quit(); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/ConnectWindow.hpp b/src/gui/ConnectWindow.hpp new file mode 100644 index 00000000..08560361 --- /dev/null +++ b/src/gui/ConnectWindow.hpp @@ -0,0 +1,116 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_CONNECTWINDOW_HPP +#define INGEN_GUI_CONNECTWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/image.h> +#include <gtkmm/label.h> +#include <gtkmm/progressbar.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/spinbutton.h> + +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +#include "Window.hpp" + +namespace Ingen { +namespace GUI { + +class App; + +/** The initially visible "Connect to engine" window. + * + * This handles actually connecting to the engine and making sure everything + * is ready before really launching the app. + * + * \ingroup GUI + */ +class ConnectWindow : public Dialog +{ +public: + ConnectWindow(BaseObjectType* cobject, + Glib::RefPtr<Gtk::Builder> xml); + + void set_connected_to(SPtr<Ingen::Interface> engine); + void start(App& app, Ingen::World* world); + + bool attached() const { return _finished_connecting; } + bool quit_flag() const { return _quit_flag; } + +private: + enum class Mode { CONNECT_REMOTE, LAUNCH_REMOTE, INTERNAL }; + + void message(const Message& msg); + + void error(const std::string& msg); + + void ingen_response(int32_t id, Status status, const std::string& subject); + + void server_toggled(); + void launch_toggled(); + void internal_toggled(); + + void disconnect(); + void next_stage(); + bool connect_remote(const URI& uri); + void connect(bool existing); + void activate(); + void deactivate(); + void quit_clicked(); + void on_show(); + void on_hide(); + + void load_widgets(); + void set_connecting_widget_states(); + + bool gtk_callback(); + void quit(); + + const Glib::RefPtr<Gtk::Builder> _xml; + + Gtk::Image* _icon; + Gtk::ProgressBar* _progress_bar; + Gtk::Label* _progress_label; + Gtk::Entry* _url_entry; + Gtk::RadioButton* _server_radio; + Gtk::SpinButton* _port_spinbutton; + Gtk::RadioButton* _launch_radio; + Gtk::RadioButton* _internal_radio; + Gtk::Button* _activate_button; + Gtk::Button* _deactivate_button; + Gtk::Button* _disconnect_button; + Gtk::Button* _connect_button; + Gtk::Button* _quit_button; + + Mode _mode; + URI _connect_uri; + int32_t _ping_id; + bool _attached; + bool _finished_connecting; + bool _widgets_loaded; + int _connect_stage; + bool _quit_flag; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_CONNECTWINDOW_HPP diff --git a/src/gui/GraphBox.cpp b/src/gui/GraphBox.cpp new file mode 100644 index 00000000..6f9969be --- /dev/null +++ b/src/gui/GraphBox.cpp @@ -0,0 +1,922 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <sstream> +#include <string> + +#include <boost/format.hpp> +#include <glib/gstdio.h> +#include <glibmm/fileutils.h> +#include <gtkmm/stock.h> + +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "BreadCrumbs.hpp" +#include "ConnectWindow.hpp" +#include "GraphCanvas.hpp" +#include "GraphTreeWindow.hpp" +#include "GraphView.hpp" +#include "GraphWindow.hpp" +#include "LoadGraphWindow.hpp" +#include "LoadPluginWindow.hpp" +#include "MessagesWindow.hpp" +#include "NewSubgraphWindow.hpp" +#include "Style.hpp" +#include "ThreadedLoader.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" +#include "ingen_config.h" + +#ifdef HAVE_WEBKIT +#include <webkit/webkit.h> +#endif + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +static const int STATUS_CONTEXT_ENGINE = 0; +static const int STATUS_CONTEXT_GRAPH = 1; +static const int STATUS_CONTEXT_HOVER = 2; + +GraphBox::GraphBox(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::VBox(cobject) + , _app(nullptr) + , _window(nullptr) + , _breadcrumbs(nullptr) + , _has_shown_documentation(false) + , _enable_signal(true) +{ + property_visible() = false; + + xml->get_widget("graph_win_alignment", _alignment); + xml->get_widget("graph_win_status_bar", _status_bar); + xml->get_widget("graph_import_menuitem", _menu_import); + xml->get_widget("graph_save_menuitem", _menu_save); + xml->get_widget("graph_save_as_menuitem", _menu_save_as); + xml->get_widget("graph_export_image_menuitem", _menu_export_image); + xml->get_widget("graph_redo_menuitem", _menu_redo); + xml->get_widget("graph_undo_menuitem", _menu_undo); + xml->get_widget("graph_cut_menuitem", _menu_cut); + xml->get_widget("graph_copy_menuitem", _menu_copy); + xml->get_widget("graph_paste_menuitem", _menu_paste); + xml->get_widget("graph_delete_menuitem", _menu_delete); + xml->get_widget("graph_select_all_menuitem", _menu_select_all); + xml->get_widget("graph_close_menuitem", _menu_close); + xml->get_widget("graph_quit_menuitem", _menu_quit); + xml->get_widget("graph_view_control_window_menuitem", _menu_view_control_window); + xml->get_widget("graph_view_engine_window_menuitem", _menu_view_engine_window); + xml->get_widget("graph_properties_menuitem", _menu_view_graph_properties); + xml->get_widget("graph_parent_menuitem", _menu_parent); + xml->get_widget("graph_refresh_menuitem", _menu_refresh); + xml->get_widget("graph_fullscreen_menuitem", _menu_fullscreen); + xml->get_widget("graph_animate_signals_menuitem", _menu_animate_signals); + xml->get_widget("graph_sprung_layout_menuitem", _menu_sprung_layout); + xml->get_widget("graph_human_names_menuitem", _menu_human_names); + xml->get_widget("graph_show_port_names_menuitem", _menu_show_port_names); + xml->get_widget("graph_zoom_in_menuitem", _menu_zoom_in); + xml->get_widget("graph_zoom_out_menuitem", _menu_zoom_out); + xml->get_widget("graph_zoom_normal_menuitem", _menu_zoom_normal); + xml->get_widget("graph_zoom_full_menuitem", _menu_zoom_full); + xml->get_widget("graph_increase_font_size_menuitem", _menu_increase_font_size); + xml->get_widget("graph_decrease_font_size_menuitem", _menu_decrease_font_size); + xml->get_widget("graph_normal_font_size_menuitem", _menu_normal_font_size); + xml->get_widget("graph_doc_pane_menuitem", _menu_show_doc_pane); + xml->get_widget("graph_status_bar_menuitem", _menu_show_status_bar); + xml->get_widget("graph_arrange_menuitem", _menu_arrange); + xml->get_widget("graph_view_messages_window_menuitem", _menu_view_messages_window); + xml->get_widget("graph_view_graph_tree_window_menuitem", _menu_view_graph_tree_window); + xml->get_widget("graph_help_about_menuitem", _menu_help_about); + xml->get_widget("graph_documentation_paned", _doc_paned); + xml->get_widget("graph_documentation_scrolledwindow", _doc_scrolledwindow); + + _menu_view_control_window->property_sensitive() = false; + _menu_import->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_import)); + _menu_save->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_save)); + _menu_save_as->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_save_as)); + _menu_export_image->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_export_image)); + _menu_redo->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_redo)); + _menu_undo->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_undo)); + _menu_copy->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_copy)); + _menu_paste->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_paste)); + _menu_delete->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_delete)); + _menu_select_all->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_select_all)); + _menu_close->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_close)); + _menu_quit->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_quit)); + _menu_parent->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_parent_activated)); + _menu_refresh->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_refresh_activated)); + _menu_fullscreen->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_fullscreen_toggled)); + _menu_animate_signals->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_animate_signals_toggled)); + _menu_sprung_layout->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_sprung_layout_toggled)); + _menu_human_names->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_human_names_toggled)); + _menu_show_doc_pane->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_doc_pane_toggled)); + _menu_show_status_bar->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_status_bar_toggled)); + _menu_show_port_names->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_port_names_toggled)); + _menu_arrange->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_arrange)); + _menu_zoom_in->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_zoom_in)); + _menu_zoom_out->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_zoom_out)); + _menu_zoom_normal->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_zoom_normal)); + _menu_zoom_full->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_zoom_full)); + _menu_increase_font_size->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_increase_font_size)); + _menu_decrease_font_size->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_decrease_font_size)); + _menu_normal_font_size->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_normal_font_size)); + _menu_view_engine_window->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_show_engine)); + _menu_view_graph_properties->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_show_properties)); + + Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get(); + clipboard->signal_owner_change().connect( + sigc::mem_fun(this, &GraphBox::event_clipboard_changed)); + +#ifdef __APPLE__ + _menu_paste->set_sensitive(true); +#endif + + _status_label = Gtk::manage(new Gtk::Label("STATUS")); + _status_bar->pack_start(*_status_label, false, true, 0); + _status_label->show(); +} + +GraphBox::~GraphBox() +{ + delete _breadcrumbs; +} + +SPtr<GraphBox> +GraphBox::create(App& app, SPtr<const GraphModel> graph) +{ + GraphBox* result = nullptr; + Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create("graph_win"); + xml->get_widget_derived("graph_win_vbox", result); + result->init_box(app); + result->set_graph(graph, SPtr<GraphView>()); + + if (app.is_plugin()) { + result->_menu_close->set_sensitive(false); + result->_menu_quit->set_sensitive(false); + } + + return SPtr<GraphBox>(result); +} + +void +GraphBox::init_box(App& app) +{ + _app = &app; + + const URI engine_uri(_app->interface()->uri()); + if (engine_uri == "ingen:/clients/event_writer") { + _status_bar->push("Running internal engine", STATUS_CONTEXT_ENGINE); + } else { + _status_bar->push( + (fmt("Connected to %1%") % engine_uri).str(), + STATUS_CONTEXT_ENGINE); + } + + _menu_view_messages_window->signal_activate().connect( + sigc::mem_fun<void>(_app->messages_dialog(), &MessagesWindow::present)); + _menu_view_graph_tree_window->signal_activate().connect( + sigc::mem_fun<void>(_app->graph_tree(), &GraphTreeWindow::present)); + + _menu_help_about->signal_activate().connect( + sigc::hide_return(sigc::mem_fun(_app, &App::show_about))); + + _breadcrumbs = new BreadCrumbs(*_app); + _breadcrumbs->signal_graph_selected.connect( + sigc::mem_fun(this, &GraphBox::set_graph_from_path)); + + _status_label->set_markup(app.status_text()); + app.signal_status_text_changed.connect( + sigc::mem_fun(*this, &GraphBox::set_status_text)); +} + +void +GraphBox::set_status_text(const std::string& text) +{ + _status_label->set_markup(text); +} + +void +GraphBox::set_graph_from_path(const Raul::Path& path, SPtr<GraphView> view) +{ + if (view) { + assert(view->graph()->path() == path); + _app->window_factory()->present_graph(view->graph(), _window, view); + } else { + SPtr<const GraphModel> model = dynamic_ptr_cast<const GraphModel>( + _app->store()->object(path)); + if (model) { + _app->window_factory()->present_graph(model, _window); + } + } +} + +/** Sets the graph for this box and initializes everything. + * + * If `view` is NULL, a new view will be created. + */ +void +GraphBox::set_graph(SPtr<const GraphModel> graph, + SPtr<GraphView> view) +{ + if (!graph || graph == _graph) { + return; + } + + _enable_signal = false; + + new_port_connection.disconnect(); + removed_port_connection.disconnect(); + edit_mode_connection.disconnect(); + _entered_connection.disconnect(); + _left_connection.disconnect(); + + _status_bar->pop(STATUS_CONTEXT_GRAPH); + + _graph = graph; + _view = view; + + if (!_view) { + _view = _breadcrumbs->view(graph->path()); + } + + if (!_view) { + _view = GraphView::create(*_app, graph); + } + + assert(_view); + + graph->signal_property().connect( + sigc::mem_fun(this, &GraphBox::property_changed)); + + if (!_view->canvas()->supports_sprung_layout()) { + _menu_sprung_layout->set_active(false); + _menu_sprung_layout->set_sensitive(false); + } + + // Add view to our alignment + if (_view->get_parent()) { + _view->get_parent()->remove(*_view.get()); + } + + _alignment->remove(); + _alignment->add(*_view.get()); + + if (_breadcrumbs->get_parent()) { + _breadcrumbs->get_parent()->remove(*_breadcrumbs); + } + + _view->breadcrumb_container()->remove(); + _view->breadcrumb_container()->add(*_breadcrumbs); + _view->breadcrumb_container()->show(); + + _breadcrumbs->build(graph->path(), _view); + _breadcrumbs->show(); + + _menu_view_control_window->property_sensitive() = false; + + for (const auto& p : graph->ports()) { + if (_app->can_control(p.get())) { + _menu_view_control_window->property_sensitive() = true; + break; + } + } + + _menu_parent->property_sensitive() = bool(graph->parent()); + + new_port_connection = graph->signal_new_port().connect( + sigc::mem_fun(this, &GraphBox::graph_port_added)); + removed_port_connection = graph->signal_removed_port().connect( + sigc::mem_fun(this, &GraphBox::graph_port_removed)); + + show(); + _alignment->show_all(); + + _menu_human_names->set_active( + _app->world()->conf().option("human-names").get<int32_t>()); + _menu_show_port_names->set_active( + _app->world()->conf().option("port-labels").get<int32_t>()); + + _doc_paned->set_position(std::numeric_limits<int>::max()); + _doc_scrolledwindow->hide(); + + _enable_signal = true; +} + +void +GraphBox::graph_port_added(SPtr<const PortModel> port) +{ + if (port->is_input() && _app->can_control(port.get())) { + _menu_view_control_window->property_sensitive() = true; + } +} + +void +GraphBox::graph_port_removed(SPtr<const PortModel> port) +{ + if (!(port->is_input() && _app->can_control(port.get()))) { + return; + } + + for (const auto& p : _graph->ports()) { + if (p->is_input() && _app->can_control(p.get())) { + _menu_view_control_window->property_sensitive() = true; + return; + } + } + + _menu_view_control_window->property_sensitive() = false; +} + +void +GraphBox::property_changed(const URI& predicate, const Atom& value) +{ + if (predicate == _app->uris().ingen_sprungLayout) { + if (value.type() == _app->uris().forge.Bool) { + _menu_sprung_layout->set_active(value.get<int32_t>()); + } + } +} + +void +GraphBox::set_documentation(const std::string& doc, bool html) +{ + _doc_scrolledwindow->remove(); + if (doc.empty()) { + _doc_scrolledwindow->hide(); + return; + } +#ifdef HAVE_WEBKIT + WebKitWebView* view = WEBKIT_WEB_VIEW(webkit_web_view_new()); + webkit_web_view_load_html_string(view, doc.c_str(), ""); + Gtk::Widget* widget = Gtk::manage(Glib::wrap(GTK_WIDGET(view))); + _doc_scrolledwindow->add(*widget); + widget->show(); +#else + Gtk::TextView* view = Gtk::manage(new Gtk::TextView()); + view->get_buffer()->set_text(doc); + view->set_wrap_mode(Gtk::WRAP_WORD); + _doc_scrolledwindow->add(*view); + view->show(); +#endif +} + +void +GraphBox::show_status(const ObjectModel* model) +{ + std::stringstream msg; + msg << model->path(); + + const PortModel* port = nullptr; + const BlockModel* block = nullptr; + + if ((port = dynamic_cast<const PortModel*>(model))) { + show_port_status(port, port->value()); + + } else if ((block = dynamic_cast<const BlockModel*>(model))) { + const PluginModel* plugin = dynamic_cast<const PluginModel*>(block->plugin()); + if (plugin) { + msg << ((boost::format(" (%1%)") % plugin->human_name()).str()); + } + _status_bar->push(msg.str(), STATUS_CONTEXT_HOVER); + } +} + +void +GraphBox::show_port_status(const PortModel* port, const Atom& value) +{ + std::stringstream msg; + msg << port->path(); + + const BlockModel* parent = dynamic_cast<const BlockModel*>(port->parent().get()); + if (parent) { + const PluginModel* plugin = dynamic_cast<const PluginModel*>(parent->plugin()); + if (plugin) { + const std::string& human_name = plugin->port_human_name(port->index()); + if (!human_name.empty()) { + msg << " (" << human_name << ")"; + } + } + } + + if (value.is_valid()) { + msg << " = " << _app->forge().str(value, true); + } + + _status_bar->pop(STATUS_CONTEXT_HOVER); + _status_bar->push(msg.str(), STATUS_CONTEXT_HOVER); +} + +void +GraphBox::object_entered(const ObjectModel* model) +{ + show_status(model); +} + +void +GraphBox::object_left(const ObjectModel* model) +{ + _status_bar->pop(STATUS_CONTEXT_GRAPH); + _status_bar->pop(STATUS_CONTEXT_HOVER); +} + +void +GraphBox::event_show_engine() +{ + if (_graph) { + _app->connect_window()->show(); + } +} + +void +GraphBox::event_clipboard_changed(GdkEventOwnerChange* ev) +{ + Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get(); + _menu_paste->set_sensitive(clipboard->wait_is_text_available()); +} + +void +GraphBox::event_show_properties() +{ + _app->window_factory()->present_properties(_graph); +} + +void +GraphBox::event_import() +{ + _app->window_factory()->present_load_graph(_graph); +} + +void +GraphBox::event_save() +{ + const Atom& document = _graph->get_property(_app->uris().ingen_file); + if (!document.is_valid() || document.type() != _app->uris().forge.URI) { + event_save_as(); + } else { + save_graph(URI(document.ptr<char>())); + } +} + +void +GraphBox::error(const Glib::ustring& message, + const Glib::ustring& secondary_text) +{ + Gtk::MessageDialog dialog( + message, true, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dialog.set_secondary_text(secondary_text); + if (_window) { + dialog.set_transient_for(*_window); + } + dialog.run(); +} + +bool +GraphBox::confirm(const Glib::ustring& message, + const Glib::ustring& secondary_text) +{ + Gtk::MessageDialog dialog( + message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true); + dialog.set_secondary_text(secondary_text); + if (_window) { + dialog.set_transient_for(*_window); + } + return dialog.run() == Gtk::RESPONSE_YES; +} + +void +GraphBox::save_graph(const URI& uri) +{ + if (_app->interface()->uri().string().substr(0, 3) == "tcp") { + _status_bar->push( + (boost::format("Saved %1% to %2% on client") + % _graph->path() % uri).str(), + STATUS_CONTEXT_GRAPH); + _app->loader()->save_graph(_graph, uri); + } else { + _status_bar->push( + (boost::format("Saved %1% to %2% on server") + % _graph->path() % uri).str(), + STATUS_CONTEXT_GRAPH); + _app->interface()->copy(_graph->uri(), uri); + } +} + +void +GraphBox::event_save_as() +{ + const URIs& uris = _app->uris(); + while (true) { + Gtk::FileChooserDialog dialog( + "Save Graph", Gtk::FILE_CHOOSER_ACTION_SAVE); + if (_window) { + dialog.set_transient_for(*_window); + } + + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + Gtk::Button* save_button = dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + save_button->property_has_default() = true; + + Gtk::FileFilter filt; + filt.add_pattern("*.ingen"); + filt.set_name("Ingen bundles"); + dialog.set_filter(filt); + + // Set current folder to most sensible default + const Atom& document = _graph->get_property(uris.ingen_file); + const Atom& dir = _app->world()->conf().option("graph-directory"); + if (document.type() == uris.forge.URI) { + dialog.set_uri(document.ptr<char>()); + } else if (dir.is_valid()) { + dialog.set_current_folder(dir.ptr<char>()); + } + + if (dialog.run() != Gtk::RESPONSE_OK) { + break; + } + + std::string filename = dialog.get_filename(); + std::string basename = Glib::path_get_basename(filename); + + if (basename.find('.') == std::string::npos) { + filename += ".ingen"; + basename += ".ingen"; + } else if (filename.substr(filename.length() - 4) == ".ttl") { + const Glib::ustring dir = Glib::path_get_dirname(filename); + if (dir.substr(dir.length() - 6) != ".ingen") { + error("<b>File does not appear to be in an Ingen bundle."); + } + } else if (filename.substr(filename.length() - 6) != ".ingen") { + error("<b>Ingen bundles must end in \".ingen\"</b>"); + continue; + } + + const std::string symbol(basename.substr(0, basename.find('.'))); + + if (!Raul::Symbol::is_valid(symbol)) { + error( + "<b>Ingen bundle names must be valid symbols.</b>", + "All characters must be _, a-z, A-Z, or 0-9, but the first may not be 0-9."); + continue; + } + + //_graph->set_property(uris.lv2_symbol, Atom(symbol.c_str())); + + bool confirmed = true; + if (Glib::file_test(filename, Glib::FILE_TEST_IS_DIR)) { + if (Glib::file_test(Glib::build_filename(filename, "manifest.ttl"), + Glib::FILE_TEST_EXISTS)) { + confirmed = confirm( + (boost::format("<b>The bundle \"%1%\" already exists." + " Replace it?</b>") % basename).str()); + } else { + confirmed = confirm( + (boost::format("<b>A directory named \"%1%\" already exists," + "but is not an Ingen bundle. " + "Save into it anyway?</b>") % basename).str(), + "This will create at least 2 .ttl files in this directory," + "and possibly several more files and/or directories, recursively. " + "Existing files will be overwritten."); + } + } else if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) { + confirmed = confirm( + (boost::format("<b>A file named \"%1%\" already exists. " + "Replace it with an Ingen bundle?</b>") + % basename).str()); + if (confirmed) { + ::g_remove(filename.c_str()); + } + } + + if (confirmed) { + const Glib::ustring uri = Glib::filename_to_uri(filename); + save_graph(URI(uri)); + + const_cast<GraphModel*>(_graph.get())->set_property( + uris.ingen_file, + _app->forge().alloc_uri(uri.c_str())); + } + + _app->world()->conf().set( + "graph-directory", + _app->world()->forge().alloc(dialog.get_current_folder())); + + break; + } +} + +void +GraphBox::event_export_image() +{ + Gtk::FileChooserDialog dialog("Export Image", Gtk::FILE_CHOOSER_ACTION_SAVE); + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + dialog.set_default_response(Gtk::RESPONSE_OK); + if (_window) { + dialog.set_transient_for(*_window); + } + + typedef std::map<std::string, std::string> Types; + Types types; + types["*.dot"] = "Graphviz DOT"; + types["*.pdf"] = "Portable Document Format"; + types["*.ps"] = "PostScript"; + types["*.svg"] = "Scalable Vector Graphics"; + for (Types::const_iterator t = types.begin(); t != types.end(); ++t) { + Gtk::FileFilter filt; + filt.add_pattern(t->first); + filt.set_name(t->second); + dialog.add_filter(filt); + if (t->first == "*.pdf") { + dialog.set_filter(filt); + } + } + + Gtk::CheckButton* bg_but = new Gtk::CheckButton("Draw _Background", true); + Gtk::Alignment* extra = new Gtk::Alignment(1.0, 0.5, 0.0, 0.0); + bg_but->set_active(true); + extra->add(*Gtk::manage(bg_but)); + extra->show_all(); + dialog.set_extra_widget(*Gtk::manage(extra)); + + if (dialog.run() == Gtk::RESPONSE_OK) { + const std::string filename = dialog.get_filename(); + if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) { + Gtk::MessageDialog confirm( + std::string("File exists! Overwrite ") + filename + "?", + true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true); + confirm.set_transient_for(dialog); + if (confirm.run() != Gtk::RESPONSE_YES) { + return; + } + } + _view->canvas()->export_image(filename.c_str(), bg_but->get_active()); + _status_bar->push((boost::format("Rendered %1% to %2%") + % _graph->path() % filename).str(), + STATUS_CONTEXT_GRAPH); + } +} + +void +GraphBox::event_copy() +{ + if (_view) { + _view->canvas()->copy_selection(); + } +} + +void +GraphBox::event_redo() +{ + _app->interface()->redo(); +} + +void +GraphBox::event_undo() +{ + _app->interface()->undo(); +} + +void +GraphBox::event_paste() +{ + if (_view) { + _view->canvas()->paste(); + } +} + +void +GraphBox::event_delete() +{ + if (_view) { + _view->canvas()->destroy_selection(); + } +} + +void +GraphBox::event_select_all() +{ + if (_view) { + _view->canvas()->select_all(); + } +} + +void +GraphBox::event_close() +{ + if (_window) { + _app->window_factory()->remove_graph_window(_window); + } +} + +void +GraphBox::event_quit() +{ + _app->quit(_window); +} + +void +GraphBox::event_zoom_in() +{ + _view->canvas()->set_font_size(_view->canvas()->get_font_size() + 1.0); +} + +void +GraphBox::event_zoom_out() +{ + _view->canvas()->set_font_size(_view->canvas()->get_font_size() - 1.0); +} + +void +GraphBox::event_zoom_normal() +{ + _view->canvas()->set_zoom(1.0); +} + +void +GraphBox::event_zoom_full() +{ + _view->canvas()->zoom_full(); +} + +void +GraphBox::event_increase_font_size() +{ + _view->canvas()->set_font_size(_view->canvas()->get_font_size() + 1.0); +} +void +GraphBox::event_decrease_font_size() +{ + _view->canvas()->set_font_size(_view->canvas()->get_font_size() - 1.0); +} +void +GraphBox::event_normal_font_size() +{ + _view->canvas()->set_font_size(_view->canvas()->get_default_font_size()); +} + +void +GraphBox::event_arrange() +{ + _app->interface()->bundle_begin(); + _view->canvas()->arrange(); + _app->interface()->bundle_end(); +} + +void +GraphBox::event_parent_activated() +{ + SPtr<Client::GraphModel> parent = dynamic_ptr_cast<Client::GraphModel>(_graph->parent()); + if (parent) { + _app->window_factory()->present_graph(parent, _window); + } +} + +void +GraphBox::event_refresh_activated() +{ + _app->interface()->get(_graph->uri()); +} + +void +GraphBox::event_fullscreen_toggled() +{ + // FIXME: ugh, use GTK signals to track state and know for sure + static bool is_fullscreen = false; + + if (_window) { + if (!is_fullscreen) { + _window->fullscreen(); + is_fullscreen = true; + } else { + _window->unfullscreen(); + is_fullscreen = false; + } + } +} + +void +GraphBox::event_doc_pane_toggled() +{ + if (_menu_show_doc_pane->get_active()) { + _doc_scrolledwindow->show_all(); + if (!_has_shown_documentation) { + const Gtk::Allocation allocation = get_allocation(); + _doc_paned->set_position(allocation.get_width() / 1.61803399); + _has_shown_documentation = true; + } + } else { + _doc_scrolledwindow->hide(); + } +} + +void +GraphBox::event_status_bar_toggled() +{ + if (_menu_show_status_bar->get_active()) { + _status_bar->show(); + } else { + _status_bar->hide(); + } +} + +void +GraphBox::event_animate_signals_toggled() +{ + _app->interface()->set_property( + URI("ingen:/clients/this"), + _app->uris().ingen_broadcast, + _app->forge().make((bool)_menu_animate_signals->get_active())); +} + +void +GraphBox::event_sprung_layout_toggled() +{ + const bool sprung = _menu_sprung_layout->get_active(); + + _view->canvas()->set_sprung_layout(sprung); + + Properties properties; + properties.emplace(_app->uris().ingen_sprungLayout, + _app->forge().make(sprung)); + _app->interface()->put(_graph->uri(), properties); +} + +void +GraphBox::event_human_names_toggled() +{ + _view->canvas()->show_human_names(_menu_human_names->get_active()); + _app->world()->conf().set( + "human-names", + _app->world()->forge().make(_menu_human_names->get_active())); +} + +void +GraphBox::event_port_names_toggled() +{ + _app->world()->conf().set( + "port-labels", + _app->world()->forge().make(_menu_show_port_names->get_active())); + if (_menu_show_port_names->get_active()) { + _view->canvas()->set_direction(GANV_DIRECTION_RIGHT); + _view->canvas()->show_port_names(true); + } else { + _view->canvas()->set_direction(GANV_DIRECTION_DOWN); + _view->canvas()->show_port_names(false); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphBox.hpp b/src/gui/GraphBox.hpp new file mode 100644 index 00000000..fd9bf9c0 --- /dev/null +++ b/src/gui/GraphBox.hpp @@ -0,0 +1,213 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPH_BOX_HPP +#define INGEN_GUI_GRAPH_BOX_HPP + +#include <string> + +#include <gtkmm/alignment.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/menushell.h> +#include <gtkmm/messagedialog.h> +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/statusbar.h> + +#include "ingen/ingen.h" +#include "ingen/types.hpp" + +#include "Window.hpp" + +namespace Raul { +class Atom; +class Path; +} + +namespace Ingen { + +class URI; + +namespace Client { +class GraphModel; +class PortModel; +class ObjectModel; +} + +namespace GUI { + +class BreadCrumbs; +class LoadGraphBox; +class LoadPluginWindow; +class NewSubgraphWindow; +class GraphDescriptionWindow; +class GraphView; +class GraphWindow; +class SubgraphModule; + +/** A window for a graph. + * + * \ingroup GUI + */ +class INGEN_API GraphBox : public Gtk::VBox +{ +public: + GraphBox(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + ~GraphBox(); + + static SPtr<GraphBox> create( + App& app, SPtr<const Client::GraphModel> graph); + + void init_box(App& app); + + void set_status_text(const std::string& text); + + void set_graph(SPtr<const Client::GraphModel> graph, + SPtr<GraphView> view); + + void set_window(GraphWindow* win) { _window = win; } + + bool documentation_is_visible() { return _doc_scrolledwindow->is_visible(); } + void set_documentation(const std::string& doc, bool html); + + SPtr<const Client::GraphModel> graph() const { return _graph; } + SPtr<GraphView> view() const { return _view; } + + void show_port_status(const Client::PortModel* port, + const Atom& value); + + void set_graph_from_path(const Raul::Path& path, SPtr<GraphView> view); + + void object_entered(const Client::ObjectModel* model); + void object_left(const Client::ObjectModel* model); + +private: + void graph_port_added(SPtr<const Client::PortModel> port); + void graph_port_removed(SPtr<const Client::PortModel> port); + void property_changed(const URI& predicate, const Atom& value); + void show_status(const Client::ObjectModel* model); + + void error(const Glib::ustring& message, + const Glib::ustring& secondary_text=""); + + bool confirm(const Glib::ustring& message, + const Glib::ustring& secondary_text=""); + + void save_graph(const URI& uri); + + void event_import(); + void event_save(); + void event_save_as(); + void event_export_image(); + void event_redo(); + void event_undo(); + void event_copy(); + void event_paste(); + void event_delete(); + void event_select_all(); + void event_close(); + void event_quit(); + void event_parent_activated(); + void event_refresh_activated(); + void event_fullscreen_toggled(); + void event_doc_pane_toggled(); + void event_status_bar_toggled(); + void event_animate_signals_toggled(); + void event_sprung_layout_toggled(); + void event_human_names_toggled(); + void event_port_names_toggled(); + void event_zoom_in(); + void event_zoom_out(); + void event_zoom_normal(); + void event_zoom_full(); + void event_increase_font_size(); + void event_decrease_font_size(); + void event_normal_font_size(); + void event_arrange(); + void event_show_properties(); + void event_show_engine(); + void event_clipboard_changed(GdkEventOwnerChange* ev); + + App* _app; + SPtr<const Client::GraphModel> _graph; + SPtr<GraphView> _view; + GraphWindow* _window; + + sigc::connection new_port_connection; + sigc::connection removed_port_connection; + sigc::connection edit_mode_connection; + + Gtk::MenuItem* _menu_import; + Gtk::MenuItem* _menu_save; + Gtk::MenuItem* _menu_save_as; + Gtk::MenuItem* _menu_export_image; + Gtk::MenuItem* _menu_redo; + Gtk::MenuItem* _menu_undo; + Gtk::MenuItem* _menu_cut; + Gtk::MenuItem* _menu_copy; + Gtk::MenuItem* _menu_paste; + Gtk::MenuItem* _menu_delete; + Gtk::MenuItem* _menu_select_all; + Gtk::MenuItem* _menu_close; + Gtk::MenuItem* _menu_quit; + Gtk::CheckMenuItem* _menu_animate_signals; + Gtk::CheckMenuItem* _menu_sprung_layout; + Gtk::CheckMenuItem* _menu_human_names; + Gtk::CheckMenuItem* _menu_show_port_names; + Gtk::CheckMenuItem* _menu_show_doc_pane; + Gtk::CheckMenuItem* _menu_show_status_bar; + Gtk::MenuItem* _menu_zoom_in; + Gtk::MenuItem* _menu_zoom_out; + Gtk::MenuItem* _menu_zoom_normal; + Gtk::MenuItem* _menu_zoom_full; + Gtk::MenuItem* _menu_increase_font_size; + Gtk::MenuItem* _menu_decrease_font_size; + Gtk::MenuItem* _menu_normal_font_size; + Gtk::MenuItem* _menu_parent; + Gtk::MenuItem* _menu_refresh; + Gtk::MenuItem* _menu_fullscreen; + Gtk::MenuItem* _menu_arrange; + Gtk::MenuItem* _menu_view_engine_window; + Gtk::MenuItem* _menu_view_control_window; + Gtk::MenuItem* _menu_view_graph_properties; + Gtk::MenuItem* _menu_view_messages_window; + Gtk::MenuItem* _menu_view_graph_tree_window; + Gtk::MenuItem* _menu_help_about; + + Gtk::Alignment* _alignment; + BreadCrumbs* _breadcrumbs; + Gtk::Statusbar* _status_bar; + Gtk::Label* _status_label; + + Gtk::HPaned* _doc_paned; + Gtk::ScrolledWindow* _doc_scrolledwindow; + + sigc::connection _entered_connection; + sigc::connection _left_connection; + + /** Invisible bin used to store breadcrumbs when not shown by a view */ + Gtk::Alignment _breadcrumb_bin; + + bool _has_shown_documentation; + bool _enable_signal; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPH_BOX_HPP diff --git a/src/gui/GraphCanvas.cpp b/src/gui/GraphCanvas.cpp new file mode 100644 index 00000000..a17915a5 --- /dev/null +++ b/src/gui/GraphCanvas.cpp @@ -0,0 +1,898 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <algorithm> +#include <cassert> +#include <map> +#include <set> +#include <string> + +#include <boost/optional.hpp> +#include <gtkmm/stock.h> + +#include "ganv/Canvas.hpp" +#include "ganv/Circle.hpp" +#include "ingen/ClashAvoider.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/World.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" + +#include "App.hpp" +#include "Arc.hpp" +#include "GraphCanvas.hpp" +#include "GraphPortModule.hpp" +#include "GraphWindow.hpp" +#include "LoadPluginWindow.hpp" +#include "NewSubgraphWindow.hpp" +#include "NodeModule.hpp" +#include "PluginMenu.hpp" +#include "Port.hpp" +#include "SubgraphModule.hpp" +#include "ThreadedLoader.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +using std::string; + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +static int +port_order(const GanvPort* a, const GanvPort* b, void* data) +{ + const Port* pa = dynamic_cast<const Port*>(Glib::wrap(a)); + const Port* pb = dynamic_cast<const Port*>(Glib::wrap(b)); + if (pa && pb) { + return ((int)pa->model()->index() - (int)pb->model()->index()); + } + return 0; +} + +GraphCanvas::GraphCanvas(App& app, + SPtr<const GraphModel> graph, + int width, + int height) + : Canvas(width, height) + , _app(app) + , _graph(std::move(graph)) + , _auto_position_count(0) + , _menu_x(0) + , _menu_y(0) + , _paste_count(0) + , _menu(nullptr) + , _internal_menu(nullptr) + , _plugin_menu(nullptr) + , _human_names(true) + , _show_port_names(true) + , _menu_dirty(false) +{ + Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create("canvas_menu"); + xml->get_widget("canvas_menu", _menu); + + xml->get_widget("canvas_menu_add_audio_input", _menu_add_audio_input); + xml->get_widget("canvas_menu_add_audio_output", _menu_add_audio_output); + xml->get_widget("canvas_menu_add_cv_input", _menu_add_cv_input); + xml->get_widget("canvas_menu_add_cv_output", _menu_add_cv_output); + xml->get_widget("canvas_menu_add_control_input", _menu_add_control_input); + xml->get_widget("canvas_menu_add_control_output", _menu_add_control_output); + xml->get_widget("canvas_menu_add_event_input", _menu_add_event_input); + xml->get_widget("canvas_menu_add_event_output", _menu_add_event_output); + xml->get_widget("canvas_menu_load_plugin", _menu_load_plugin); + xml->get_widget("canvas_menu_load_graph", _menu_load_graph); + xml->get_widget("canvas_menu_new_graph", _menu_new_graph); + xml->get_widget("canvas_menu_edit", _menu_edit); + xml->get_widget("canvas_menu_properties", _menu_properties); + + const URIs& uris = _app.uris(); + + // Add port menu items + _menu_add_audio_input->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "audio_in", "Audio In", uris.lv2_AudioPort, false)); + _menu_add_audio_output->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "audio_out", "Audio Out", uris.lv2_AudioPort, true)); + _menu_add_cv_input->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "cv_in", "CV In", uris.lv2_CVPort, false)); + _menu_add_cv_output->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "cv_out", "CV Out", uris.lv2_CVPort, true)); + _menu_add_control_input->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "control_in", "Control In", uris.lv2_ControlPort, false)); + _menu_add_control_output->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "control_out", "Control Out", uris.lv2_ControlPort, true)); + _menu_add_event_input->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "event_in", "Event In", uris.atom_AtomPort, false)); + _menu_add_event_output->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "event_out", "Event Out", uris.atom_AtomPort, true)); + + signal_event.connect( + sigc::mem_fun(this, &GraphCanvas::on_event)); + signal_connect.connect( + sigc::mem_fun(this, &GraphCanvas::connect)); + signal_disconnect.connect( + sigc::mem_fun(this, &GraphCanvas::disconnect)); + + // Connect to model signals to track state + _graph->signal_new_block().connect( + sigc::mem_fun(this, &GraphCanvas::add_block)); + _graph->signal_removed_block().connect( + sigc::mem_fun(this, &GraphCanvas::remove_block)); + _graph->signal_new_port().connect( + sigc::mem_fun(this, &GraphCanvas::add_port)); + _graph->signal_removed_port().connect( + sigc::mem_fun(this, &GraphCanvas::remove_port)); + _graph->signal_new_arc().connect( + sigc::mem_fun(this, &GraphCanvas::connection)); + _graph->signal_removed_arc().connect( + sigc::mem_fun(this, &GraphCanvas::disconnection)); + + _app.store()->signal_new_plugin().connect( + sigc::mem_fun(this, &GraphCanvas::add_plugin)); + _app.store()->signal_plugin_deleted().connect( + sigc::mem_fun(this, &GraphCanvas::remove_plugin)); + + // Connect widget signals to do things + _menu_load_plugin->signal_activate().connect( + sigc::mem_fun(this, &GraphCanvas::menu_load_plugin)); + _menu_load_graph->signal_activate().connect( + sigc::mem_fun(this, &GraphCanvas::menu_load_graph)); + _menu_new_graph->signal_activate().connect( + sigc::mem_fun(this, &GraphCanvas::menu_new_graph)); + _menu_properties->signal_activate().connect( + sigc::mem_fun(this, &GraphCanvas::menu_properties)); + + show_human_names(app.world()->conf().option("human-names").get<int32_t>()); + show_port_names(app.world()->conf().option("port-labels").get<int32_t>()); + set_port_order(port_order, nullptr); +} + +void +GraphCanvas::show_menu(bool position, unsigned button, uint32_t time) +{ + _app.request_plugins_if_necessary(); + + if (!_internal_menu || _menu_dirty) { + build_menus(); + } + + if (position) { + _menu->popup(sigc::mem_fun(this, &GraphCanvas::auto_menu_position), button, time); + } else { + _menu->popup(button, time); + } +} + +void +GraphCanvas::build_menus() +{ + // Build (or clear existing) internal plugin menu + if (_internal_menu) { + _internal_menu->items().clear(); + } else { + _menu->items().push_back( + Gtk::Menu_Helpers::ImageMenuElem( + "In_ternal", + *(manage(new Gtk::Image(Gtk::Stock::EXECUTE, Gtk::ICON_SIZE_MENU))))); + Gtk::MenuItem* internal_menu_item = &(_menu->items().back()); + _internal_menu = Gtk::manage(new Gtk::Menu()); + internal_menu_item->set_submenu(*_internal_menu); + _menu->reorder_child(*internal_menu_item, 4); + } + + // Build skeleton LV2 plugin class heirarchy for 'Plugin' menu + if (_plugin_menu) { + _plugin_menu->clear(); + } else { + _plugin_menu = Gtk::manage(new PluginMenu(*_app.world())); + _menu->items().push_back( + Gtk::Menu_Helpers::ImageMenuElem( + "_Plugin", + *(manage(new Gtk::Image(Gtk::Stock::EXECUTE, Gtk::ICON_SIZE_MENU))))); + Gtk::MenuItem* plugin_menu_item = &(_menu->items().back()); + plugin_menu_item->set_submenu(*_plugin_menu); + _menu->reorder_child(*plugin_menu_item, 5); + _plugin_menu->signal_load_plugin.connect( + sigc::mem_fun(this, &GraphCanvas::load_plugin)); + } + + // Add known plugins to menu heirarchy + SPtr<const ClientStore::Plugins> plugins = _app.store()->plugins(); + for (const auto& p : *plugins.get()) { + add_plugin(p.second); + } + + _menu_dirty = false; +} + +void +GraphCanvas::build() +{ + const Store::const_range kids = _app.store()->children_range(_graph); + + // Create modules for blocks + for (Store::const_iterator i = kids.first; i != kids.second; ++i) { + SPtr<BlockModel> block = dynamic_ptr_cast<BlockModel>(i->second); + if (block && block->parent() == _graph) { + add_block(block); + } + } + + // Create pseudo modules for ports (ports on this canvas, not on our module) + for (const auto& p : _graph->ports()) { + add_port(p); + } + + // Create arcs + for (const auto& a : _graph->arcs()) { + connection(dynamic_ptr_cast<ArcModel>(a.second)); + } +} + +static void +show_module_human_names(GanvNode* node, void* data) +{ + bool b = *(bool*)data; + if (GANV_IS_MODULE(node)) { + Ganv::Module* module = Glib::wrap(GANV_MODULE(node)); + NodeModule* nmod = dynamic_cast<NodeModule*>(module); + if (nmod) { + nmod->show_human_names(b); + } + + GraphPortModule* pmod = dynamic_cast<GraphPortModule*>(module); + if (pmod) { + pmod->show_human_names(b); + } + } +} + +void +GraphCanvas::show_human_names(bool b) +{ + _human_names = b; + _app.world()->conf().set("human-names", _app.forge().make(b)); + + for_each_node(show_module_human_names, &b); +} + +static void +ensure_port_labels(GanvNode* node, void* data) +{ + if (GANV_IS_MODULE(node)) { + Ganv::Module* module = Glib::wrap(GANV_MODULE(node)); + for (Ganv::Port* p : *module) { + Ingen::GUI::Port* port = dynamic_cast<Ingen::GUI::Port*>(p); + if (port) { + port->ensure_label(); + } + } + } +} + +void +GraphCanvas::show_port_names(bool b) +{ + ganv_canvas_set_direction(gobj(), b ? GANV_DIRECTION_RIGHT : GANV_DIRECTION_DOWN); + for_each_node(ensure_port_labels, &b); +} + +void +GraphCanvas::add_plugin(SPtr<PluginModel> p) +{ + if (_internal_menu && _app.uris().ingen_Internal == p->type()) { + _internal_menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + std::string("_") + p->human_name(), + sigc::bind(sigc::mem_fun(this, &GraphCanvas::load_plugin), p))); + } else if (_plugin_menu) { + _plugin_menu->add_plugin(p); + } +} + +void +GraphCanvas::remove_plugin(const URI& uri) +{ + // Flag menus as dirty so they will be rebuilt when needed next + _menu_dirty = true; +} + +void +GraphCanvas::add_block(SPtr<const BlockModel> bm) +{ + SPtr<const GraphModel> pm = dynamic_ptr_cast<const GraphModel>(bm); + NodeModule* module; + if (pm) { + module = SubgraphModule::create(*this, pm, _human_names); + } else { + module = NodeModule::create(*this, bm, _human_names); + } + + module->show(); + _views.emplace(bm, module); + if (_pastees.find(bm->path()) != _pastees.end()) { + module->set_selected(true); + } +} + +void +GraphCanvas::remove_block(SPtr<const BlockModel> bm) +{ + auto i = _views.find(bm); + + if (i != _views.end()) { + const guint n_ports = i->second->num_ports(); + for (gint p = n_ports - 1; p >= 0; --p) { + delete i->second->get_port(p); + } + delete i->second; + _views.erase(i); + } +} + +void +GraphCanvas::add_port(SPtr<const PortModel> pm) +{ + GraphPortModule* view = GraphPortModule::create(*this, pm); + _views.emplace(pm, view); + view->show(); +} + +void +GraphCanvas::remove_port(SPtr<const PortModel> pm) +{ + auto i = _views.find(pm); + + // Port on this graph + if (i != _views.end()) { + delete i->second; + _views.erase(i); + + } else { + NodeModule* module = dynamic_cast<NodeModule*>(_views[pm->parent()]); + module->delete_port_view(pm); + } + + assert(_views.find(pm) == _views.end()); +} + +Ganv::Port* +GraphCanvas::get_port_view(SPtr<PortModel> port) +{ + Ganv::Module* module = _views[port]; + + // Port on this graph + if (module) { + GraphPortModule* ppm = dynamic_cast<GraphPortModule*>(module); + return ppm + ? *ppm->begin() + : dynamic_cast<Ganv::Port*>(module); + } else { + module = dynamic_cast<NodeModule*>(_views[port->parent()]); + if (module) { + for (const auto& p : *module) { + GUI::Port* pv = dynamic_cast<GUI::Port*>(p); + if (pv && pv->model() == port) { + return pv; + } + } + } + } + + return nullptr; +} + +/** Called when a connection is added to the model. */ +void +GraphCanvas::connection(SPtr<const ArcModel> arc) +{ + Ganv::Port* const tail = get_port_view(arc->tail()); + Ganv::Port* const head = get_port_view(arc->head()); + + if (tail && head) { + new GUI::Arc(*this, arc, tail, head); + } else { + _app.log().error(fmt("Unable to find ports to connect %1% => %2%\n") + % arc->tail_path() % arc->head_path()); + } +} + +/** Called when a connection is removed from the model. */ +void +GraphCanvas::disconnection(SPtr<const ArcModel> arc) +{ + Ganv::Port* const tail = get_port_view(arc->tail()); + Ganv::Port* const head = get_port_view(arc->head()); + + if (tail && head) { + remove_edge_between(tail, head); + if (arc->head()->is_a(_app.uris().lv2_AudioPort)) { + GUI::Port* const h = dynamic_cast<GUI::Port*>(head); + if (h) { + h->activity(_app.forge().make(0.0f)); // Reset peaks + } + } + } else { + _app.log().error(fmt("Unable to find ports to disconnect %1% => %2%\n") + % arc->tail_path() % arc->head_path()); + } +} + +/** Called when the user connects ports on the canvas. */ +void +GraphCanvas::connect(Ganv::Node* tail, Ganv::Node* head) +{ + const GUI::Port* const t = dynamic_cast<GUI::Port*>(tail); + const GUI::Port* const h = dynamic_cast<GUI::Port*>(head); + + if (t && h) { + _app.interface()->connect(t->model()->path(), h->model()->path()); + } +} + +/** Called when the user disconnects ports on the canvas. */ +void +GraphCanvas::disconnect(Ganv::Node* tail, Ganv::Node* head) +{ + const GUI::Port* const t = dynamic_cast<GUI::Port*>(tail); + const GUI::Port* const h = dynamic_cast<GUI::Port*>(head); + + if (t && h) { + _app.interface()->disconnect(t->model()->path(), h->model()->path()); + } +} + +void +GraphCanvas::auto_menu_position(int& x, int& y, bool& push_in) +{ + std::pair<int, int> scroll_offsets; + get_scroll_offsets(scroll_offsets.first, scroll_offsets.second); + + if (_auto_position_count > 1 && scroll_offsets != _auto_position_scroll_offsets) { + // Scroll changed since last auto position, reset + _menu_x = 0; + _menu_y = 0; + _auto_position_count = 0; + } + + if (_menu_x == 0 && _menu_y == 0) { + // No menu position set, start near top left of canvas + widget().translate_coordinates( + *_app.window_factory()->graph_window(_graph), + 64, 64, _menu_x, _menu_y); + + int origin_x; + int origin_y; + widget().get_window()->get_origin(origin_x, origin_y); + _menu_x += origin_x; + _menu_y += origin_y; + } + + const int cascade = _auto_position_count * 32; + + x = _menu_x + cascade; + y = _menu_y + cascade; + push_in = true; + + ++_auto_position_count; + _auto_position_scroll_offsets = scroll_offsets; +} + +bool +GraphCanvas::on_event(GdkEvent* event) +{ + assert(event); + + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 3) { + _auto_position_count = 1; + _menu_x = (int)event->button.x_root; + _menu_y = (int)event->button.y_root; + show_menu(false, event->button.button, event->button.time); + ret = true; + } + break; + + case GDK_KEY_PRESS: + switch (event->key.keyval) { + case GDK_Delete: + destroy_selection(); + ret = true; + break; + case GDK_Home: + scroll_to(0, 0); + break; + case GDK_space: + case GDK_Menu: + show_menu(true, 3, event->key.time); + default: break; + } + + case GDK_MOTION_NOTIFY: + _paste_count = 0; + break; + + default: break; + } + + return ret; +} + +void +GraphCanvas::clear_selection() +{ + GraphWindow* win = _app.window_factory()->graph_window(_graph); + if (win) { + win->set_documentation("", false); + } + + Ganv::Canvas::clear_selection(); +} + +static void +destroy_node(GanvNode* node, void* data) +{ + if (!GANV_IS_MODULE(node)) { + return; + } + + App* app = (App*)data; + Ganv::Module* module = Glib::wrap(GANV_MODULE(node)); + NodeModule* node_module = dynamic_cast<NodeModule*>(module); + + if (node_module) { + app->interface()->del(node_module->block()->uri()); + } else { + GraphPortModule* port_module = dynamic_cast<GraphPortModule*>(module); + if (port_module && + strcmp(port_module->port()->path().symbol(), "control") && + strcmp(port_module->port()->path().symbol(), "notify")) { + app->interface()->del(port_module->port()->uri()); + } + } +} + +static void +destroy_arc(GanvEdge* arc, void* data) +{ + App* app = (App*)data; + Ganv::Edge* arcmm = Glib::wrap(arc); + + Port* tail = dynamic_cast<Port*>(arcmm->get_tail()); + Port* head = dynamic_cast<Port*>(arcmm->get_head()); + app->interface()->disconnect(tail->model()->path(), head->model()->path()); +} + +void +GraphCanvas::destroy_selection() +{ + _app.interface()->bundle_begin(); + for_each_selected_edge(destroy_arc, &_app); + for_each_selected_node(destroy_node, &_app); + _app.interface()->bundle_end(); +} + +static void +serialise_node(GanvNode* node, void* data) +{ + Serialiser* serialiser = (Serialiser*)data; + if (!GANV_IS_MODULE(node)) { + return; + } + + Ganv::Module* module = Glib::wrap(GANV_MODULE(node)); + NodeModule* node_module = dynamic_cast<NodeModule*>(module); + + if (node_module) { + serialiser->serialise(node_module->block()); + } else { + GraphPortModule* port_module = dynamic_cast<GraphPortModule*>(module); + if (port_module) { + serialiser->serialise(port_module->port()); + } + } +} + +static void +serialise_arc(GanvEdge* arc, void* data) +{ + Serialiser* serialiser = (Serialiser*)data; + if (!GANV_IS_EDGE(arc)) { + return; + } + + GUI::Arc* garc = dynamic_cast<GUI::Arc*>(Glib::wrap(GANV_EDGE(arc))); + if (garc) { + serialiser->serialise_arc(Sord::Node(), garc->model()); + } +} + +void +GraphCanvas::copy_selection() +{ + std::lock_guard<std::mutex> lock(_app.world()->rdf_mutex()); + + Serialiser serialiser(*_app.world()); + serialiser.start_to_string(_graph->path(), _graph->base_uri()); + + for_each_selected_node(serialise_node, &serialiser); + for_each_selected_edge(serialise_arc, &serialiser); + + Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get(); + clipboard->set_text(serialiser.finish()); + _paste_count = 0; +} + +void +GraphCanvas::paste() +{ + typedef Properties::const_iterator PropIter; + + std::lock_guard<std::mutex> lock(_app.world()->rdf_mutex()); + + const Glib::ustring str = Gtk::Clipboard::get()->wait_for_text(); + SPtr<Parser> parser = _app.loader()->parser(); + const URIs& uris = _app.uris(); + const Raul::Path& parent = _graph->path(); + if (!parser) { + _app.log().error("Unable to load parser, paste unavailable\n"); + return; + } + + // Prepare for paste + clear_selection(); + _pastees.clear(); + ++_paste_count; + + // Make a client store to serve as clipboard + ClientStore clipboard(_app.world()->uris(), _app.log()); + clipboard.set_plugins(_app.store()->plugins()); + clipboard.put(main_uri(), + {{uris.rdf_type, Property(uris.ingen_Graph)}}); + + // Parse clipboard text into clipboard store + boost::optional<URI> base_uri = parser->parse_string( + _app.world(), &clipboard, str, main_uri()); + + // Figure out the copy graph base path + Raul::Path copy_root("/"); + if (base_uri) { + std::string base = *base_uri; + if (base[base.size() - 1] == '/') { + base = base.substr(0, base.size() - 1); + } + copy_root = uri_to_path(URI(base)); + } + + // Find the minimum x and y coordinate of objects to be pasted + float min_x = std::numeric_limits<float>::max(); + float min_y = std::numeric_limits<float>::max(); + for (const auto& c : clipboard) { + if (c.first.parent() == Raul::Path("/")) { + const Atom& x = c.second->get_property(uris.ingen_canvasX); + const Atom& y = c.second->get_property(uris.ingen_canvasY); + if (x.type() == uris.atom_Float) { + min_x = std::min(min_x, x.get<float>()); + } + if (y.type() == uris.atom_Float) { + min_y = std::min(min_y, y.get<float>()); + } + } + } + + // Find canvas paste origin based on pointer position + int widget_point_x, widget_point_y, scroll_x, scroll_y; + widget().get_pointer(widget_point_x, widget_point_y); + get_scroll_offsets(scroll_x, scroll_y); + const int paste_x = widget_point_x + scroll_x + (20.0f * _paste_count); + const int paste_y = widget_point_y + scroll_y + (20.0f * _paste_count); + + _app.interface()->bundle_begin(); + + // Put each top level object in the clipboard store + ClashAvoider avoider(*_app.store().get()); + for (const auto& c : clipboard) { + if (c.first.is_root() || c.first.parent() != Raul::Path("/")) { + continue; + } + + const SPtr<Node> node = c.second; + const Raul::Path& old_path = copy_root.child(node->path()); + const URI& old_uri = path_to_uri(old_path); + const Raul::Path& new_path = avoider.map_path(parent.child(node->path())); + + Properties props{{uris.lv2_prototype, + _app.forge().make_urid(old_uri)}}; + + // Set the same types + const auto t = node->properties().equal_range(uris.rdf_type); + props.insert(t.first, t.second); + + // Set coordinates so paste origin is at the mouse pointer + PropIter xi = node->properties().find(uris.ingen_canvasX); + PropIter yi = node->properties().find(uris.ingen_canvasY); + if (xi != node->properties().end()) { + const float x = xi->second.get<float>() - min_x + paste_x; + props.insert({xi->first, Property(_app.forge().make(x), + xi->second.context())}); + } + if (yi != node->properties().end()) { + const float y = yi->second.get<float>() - min_y + paste_y; + props.insert({yi->first, Property(_app.forge().make(y), + yi->second.context())}); + } + + _app.interface()->put(path_to_uri(new_path), props); + _pastees.insert(new_path); + } + + // Connect objects + for (auto a : clipboard.object(Raul::Path("/"))->arcs()) { + _app.interface()->connect( + avoider.map_path(parent.child(a.second->tail_path())), + avoider.map_path(parent.child(a.second->head_path()))); + } + + _app.interface()->bundle_end(); +} + +void +GraphCanvas::generate_port_name( + const string& sym_base, string& symbol, + const string& name_base, string& name) +{ + symbol = sym_base; + name = name_base; + + char num_buf[5]; + uint32_t i = 1; + for ( ; i < 9999; ++i) { + snprintf(num_buf, sizeof(num_buf), "%u", i); + symbol = sym_base + "_"; + symbol += num_buf; + if (!_graph->get_port(Raul::Symbol::symbolify(symbol))) { + break; + } + } + + assert(Raul::Path::is_valid(string("/") + symbol)); + + name.append(" ").append(num_buf); +} + +void +GraphCanvas::menu_add_port(const string& sym_base, + const string& name_base, + const URI& type, + bool is_output) +{ + string sym, name; + generate_port_name(sym_base, sym, name_base, name); + const Raul::Path& path = _graph->path().child(Raul::Symbol(sym)); + + const URIs& uris = _app.uris(); + + Properties props = get_initial_data(Resource::Graph::INTERNAL); + props.emplace(uris.rdf_type, _app.forge().make_urid(type)); + if (type == uris.atom_AtomPort) { + props.emplace(uris.atom_bufferType, Property(uris.atom_Sequence)); + } + props.emplace( + uris.rdf_type, + Property(is_output ? uris.lv2_OutputPort : uris.lv2_InputPort)); + props.emplace(uris.lv2_index, + _app.forge().make(int32_t(_graph->num_ports()))); + props.emplace(uris.lv2_name, _app.forge().alloc(name.c_str())); + _app.interface()->put(path_to_uri(path), props); +} + +void +GraphCanvas::load_plugin(WPtr<PluginModel> weak_plugin) +{ + SPtr<PluginModel> plugin = weak_plugin.lock(); + if (!plugin) { + return; + } + + Raul::Symbol symbol = plugin->default_block_symbol(); + unsigned offset = _app.store()->child_name_offset(_graph->path(), symbol); + if (offset != 0) { + std::stringstream ss; + ss << symbol << "_" << offset; + symbol = Raul::Symbol(ss.str()); + } + + const URIs& uris = _app.uris(); + const Raul::Path path = _graph->path().child(symbol); + + // FIXME: polyphony? + Properties props = get_initial_data(); + props.emplace(uris.rdf_type, Property(uris.ingen_Block)); + props.emplace(uris.lv2_prototype, uris.forge.make_urid(plugin->uri())); + _app.interface()->put(path_to_uri(path), props); +} + +/** Try to guess a suitable location for a new module. + */ +void +GraphCanvas::get_new_module_location(double& x, double& y) +{ + int scroll_x; + int scroll_y; + get_scroll_offsets(scroll_x, scroll_y); + x = scroll_x + 20; + y = scroll_y + 20; +} + +Properties +GraphCanvas::get_initial_data(Resource::Graph ctx) +{ + Properties result; + const URIs& uris = _app.uris(); + result.emplace(uris.ingen_canvasX, + Property(_app.forge().make((float)_menu_x), ctx)); + result.emplace(uris.ingen_canvasY, + Property(_app.forge().make((float)_menu_y), ctx)); + return result; +} + +void +GraphCanvas::menu_load_plugin() +{ + _app.window_factory()->present_load_plugin(_graph, get_initial_data()); +} + +void +GraphCanvas::menu_load_graph() +{ + _app.window_factory()->present_load_subgraph( + _graph, get_initial_data(Resource::Graph::EXTERNAL)); +} + +void +GraphCanvas::menu_new_graph() +{ + _app.window_factory()->present_new_subgraph( + _graph, get_initial_data(Resource::Graph::EXTERNAL)); +} + +void +GraphCanvas::menu_properties() +{ + _app.window_factory()->present_properties(_graph); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphCanvas.hpp b/src/gui/GraphCanvas.hpp new file mode 100644 index 00000000..a7340744 --- /dev/null +++ b/src/gui/GraphCanvas.hpp @@ -0,0 +1,159 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPHCANVAS_HPP +#define INGEN_GUI_GRAPHCANVAS_HPP + +#include <string> +#include <map> +#include <set> + +#include "lilv/lilv.h" + +#include "ganv/Canvas.hpp" +#include "ganv/Module.hpp" +#include "ingen/Node.hpp" +#include "ingen/client/ArcModel.hpp" +#include "ingen/types.hpp" +#include "raul/Path.hpp" + +#include "NodeModule.hpp" + +namespace Ingen { + +namespace Client { class GraphModel; } + +namespace GUI { + +class NodeModule; +class PluginMenu; + +/** Graph canvas widget. + * + * \ingroup GUI + */ +class GraphCanvas : public Ganv::Canvas +{ +public: + GraphCanvas(App& app, + SPtr<const Client::GraphModel> graph, + int width, + int height); + + virtual ~GraphCanvas() {} + + App& app() { return _app; } + + void build(); + void show_human_names(bool b); + void show_port_names(bool b); + bool show_port_names() const { return _show_port_names; } + + void add_plugin(SPtr<Client::PluginModel> p); + void remove_plugin(const URI& uri); + void add_block(SPtr<const Client::BlockModel> bm); + void remove_block(SPtr<const Client::BlockModel> bm); + void add_port(SPtr<const Client::PortModel> pm); + void remove_port(SPtr<const Client::PortModel> pm); + void connection(SPtr<const Client::ArcModel> arc); + void disconnection(SPtr<const Client::ArcModel> arc); + + void get_new_module_location(double& x, double& y); + + void clear_selection(); + void destroy_selection(); + void copy_selection(); + void paste(); + + void show_menu(bool position, unsigned button, uint32_t time); + + bool on_event(GdkEvent* event); + +private: + enum class ControlType { NUMBER, BUTTON }; + void generate_port_name( + const std::string& sym_base, std::string& symbol, + const std::string& name_base, std::string& name); + + void menu_add_port(const std::string& sym_base, + const std::string& name_base, + const URI& type, + bool is_output); + + void menu_load_plugin(); + void menu_new_graph(); + void menu_load_graph(); + void menu_properties(); + void load_plugin(WPtr<Client::PluginModel> weak_plugin); + + void build_menus(); + + void auto_menu_position(int& x, int& y, bool& push_in); + + typedef std::multimap<const std::string, const LilvPluginClass*> LV2Children; + + Properties get_initial_data(Resource::Graph ctx=Resource::Graph::DEFAULT); + + Ganv::Port* get_port_view(SPtr<Client::PortModel> port); + + void connect(Ganv::Node* tail, + Ganv::Node* head); + + void disconnect(Ganv::Node* tail, + Ganv::Node* head); + + App& _app; + SPtr<const Client::GraphModel> _graph; + + typedef std::map<SPtr<const Client::ObjectModel>, Ganv::Module*> Views; + Views _views; + + int _auto_position_count; + std::pair<int, int> _auto_position_scroll_offsets; + + int _menu_x; + int _menu_y; + int _paste_count; + + // Track pasted objects so they can be selected when they arrive + std::set<Raul::Path> _pastees; + + Gtk::Menu* _menu; + Gtk::Menu* _internal_menu; + PluginMenu* _plugin_menu; + Gtk::MenuItem* _menu_add_audio_input; + Gtk::MenuItem* _menu_add_audio_output; + Gtk::MenuItem* _menu_add_control_input; + Gtk::MenuItem* _menu_add_control_output; + Gtk::MenuItem* _menu_add_cv_input; + Gtk::MenuItem* _menu_add_cv_output; + Gtk::MenuItem* _menu_add_event_input; + Gtk::MenuItem* _menu_add_event_output; + Gtk::MenuItem* _menu_load_plugin; + Gtk::MenuItem* _menu_load_graph; + Gtk::MenuItem* _menu_new_graph; + Gtk::MenuItem* _menu_properties; + Gtk::CheckMenuItem* _menu_edit; + + bool _human_names; + bool _show_port_names; + bool _menu_dirty; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPHCANVAS_HPP diff --git a/src/gui/GraphPortModule.cpp b/src/gui/GraphPortModule.cpp new file mode 100644 index 00000000..5987b0e3 --- /dev/null +++ b/src/gui/GraphPortModule.cpp @@ -0,0 +1,166 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> +#include <utility> + +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "Style.hpp" +#include "GraphCanvas.hpp" +#include "GraphPortModule.hpp" +#include "GraphWindow.hpp" +#include "Port.hpp" +#include "PortMenu.hpp" +#include "RenameWindow.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +GraphPortModule::GraphPortModule(GraphCanvas& canvas, + SPtr<const Client::PortModel> model) + : Ganv::Module(canvas, "", 0, 0, false) // FIXME: coords? + , _model(model) + , _port(nullptr) +{ + assert(model); + + assert(dynamic_ptr_cast<const GraphModel>(model->parent())); + + set_stacked(model->polyphonic()); + if (model->is_input() && !model->is_numeric()) { + set_is_source(true); + } + + model->signal_property().connect( + sigc::mem_fun(this, &GraphPortModule::property_changed)); + + signal_moved().connect( + sigc::mem_fun(this, &GraphPortModule::store_location)); +} + +GraphPortModule* +GraphPortModule::create(GraphCanvas& canvas, + SPtr<const PortModel> model) +{ + GraphPortModule* ret = new GraphPortModule(canvas, model); + Port* port = Port::create(canvas.app(), *ret, model, true); + + ret->set_port(port); + if (model->is_numeric()) { + port->show_control(); + } + + for (const auto& p : model->properties()) { + ret->property_changed(p.first, p.second); + } + + return ret; +} + +App& +GraphPortModule::app() const +{ + return ((GraphCanvas*)canvas())->app(); +} + +bool +GraphPortModule::show_menu(GdkEventButton* ev) +{ + return _port->show_menu(ev); +} + +void +GraphPortModule::store_location(double ax, double ay) +{ + const URIs& uris = app().uris(); + + const Atom x(app().forge().make(static_cast<float>(ax))); + const Atom y(app().forge().make(static_cast<float>(ay))); + + if (x != _model->get_property(uris.ingen_canvasX) || + y != _model->get_property(uris.ingen_canvasY)) + { + app().interface()->put( + _model->uri(), + {{uris.ingen_canvasX, Property(x, Property::Graph::INTERNAL)}, + {uris.ingen_canvasY, Property(y, Property::Graph::INTERNAL)}}); + } +} + +void +GraphPortModule::show_human_names(bool b) +{ + const URIs& uris = app().uris(); + const Atom& name = _model->get_property(uris.lv2_name); + if (b && name.type() == uris.forge.String) { + set_name(name.ptr<char>()); + } else { + set_name(_model->symbol().c_str()); + } +} + +void +GraphPortModule::set_name(const std::string& n) +{ + _port->set_label(n.c_str()); +} + +void +GraphPortModule::property_changed(const URI& key, const Atom& value) +{ + const URIs& uris = app().uris(); + if (value.type() == uris.forge.Float) { + if (key == uris.ingen_canvasX) { + move_to(value.get<float>(), get_y()); + } else if (key == uris.ingen_canvasY) { + move_to(get_x(), value.get<float>()); + } + } else if (value.type() == uris.forge.String) { + if (key == uris.lv2_name && + app().world()->conf().option("human-names").get<int32_t>()) { + set_name(value.ptr<char>()); + } else if (key == uris.lv2_symbol && + !app().world()->conf().option("human-names").get<int32_t>()) { + set_name(value.ptr<char>()); + } + } else if (value.type() == uris.forge.Bool) { + if (key == uris.ingen_polyphonic) { + set_stacked(value.get<int32_t>()); + } + } +} + +void +GraphPortModule::set_selected(gboolean b) +{ + if (b != get_selected()) { + Module::set_selected(b); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphPortModule.hpp b/src/gui/GraphPortModule.hpp new file mode 100644 index 00000000..97bc2e5b --- /dev/null +++ b/src/gui/GraphPortModule.hpp @@ -0,0 +1,79 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPHPORTMODULE_HPP +#define INGEN_GUI_GRAPHPORTMODULE_HPP + +#include <string> + +#include "ganv/Module.hpp" + +#include "Port.hpp" + +namespace Raul { class Atom; } + +namespace Ingen { namespace Client { +class PortModel; +} } + +namespace Ingen { +namespace GUI { + +class GraphCanvas; +class Port; +class PortMenu; + +/** A "module" to represent a graph's port on its own canvas. + * + * Translation: This is the nameless single port pseudo module thingy. + * + * \ingroup GUI + */ +class GraphPortModule : public Ganv::Module +{ +public: + static GraphPortModule* create( + GraphCanvas& canvas, + SPtr<const Client::PortModel> model); + + App& app() const; + + virtual void store_location(double ax, double ay); + void show_human_names(bool b); + + void set_name(const std::string& n); + + SPtr<const Client::PortModel> port() const { return _model; } + +protected: + GraphPortModule(GraphCanvas& canvas, + SPtr<const Client::PortModel> model); + + bool show_menu(GdkEventButton* ev); + void set_selected(gboolean b); + + void set_port(Port* port) { _port = port; } + + void property_changed(const URI& key, const Atom& value); + + SPtr<const Client::PortModel> _model; + Port* _port; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPHPORTMODULE_HPP diff --git a/src/gui/GraphTreeWindow.cpp b/src/gui/GraphTreeWindow.cpp new file mode 100644 index 00000000..1eb6557b --- /dev/null +++ b/src/gui/GraphTreeWindow.cpp @@ -0,0 +1,235 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "App.hpp" +#include "GraphTreeWindow.hpp" +#include "SubgraphModule.hpp" +#include "WindowFactory.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "raul/Path.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +GraphTreeWindow::GraphTreeWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) + , _app(nullptr) + , _enable_signal(true) +{ + xml->get_widget_derived("graphs_treeview", _graphs_treeview); + + _graph_treestore = Gtk::TreeStore::create(_graph_tree_columns); + _graphs_treeview->set_window(this); + _graphs_treeview->set_model(_graph_treestore); + Gtk::TreeViewColumn* name_col = Gtk::manage( + new Gtk::TreeViewColumn("Graph", _graph_tree_columns.name_col)); + Gtk::TreeViewColumn* enabled_col = Gtk::manage( + new Gtk::TreeViewColumn("Run", _graph_tree_columns.enabled_col)); + name_col->set_resizable(true); + name_col->set_expand(true); + + _graphs_treeview->append_column(*name_col); + _graphs_treeview->append_column(*enabled_col); + Gtk::CellRendererToggle* enabled_renderer = dynamic_cast<Gtk::CellRendererToggle*>( + _graphs_treeview->get_column_cell_renderer(1)); + enabled_renderer->property_activatable() = true; + + _graph_tree_selection = _graphs_treeview->get_selection(); + + _graphs_treeview->signal_row_activated().connect( + sigc::mem_fun(this, &GraphTreeWindow::event_graph_activated)); + enabled_renderer->signal_toggled().connect( + sigc::mem_fun(this, &GraphTreeWindow::event_graph_enabled_toggled)); + + _graphs_treeview->columns_autosize(); +} + +void +GraphTreeWindow::init(App& app, ClientStore& store) +{ + _app = &app; + store.signal_new_object().connect( + sigc::mem_fun(this, &GraphTreeWindow::new_object)); +} + +void +GraphTreeWindow::new_object(SPtr<ObjectModel> object) +{ + SPtr<GraphModel> graph = dynamic_ptr_cast<GraphModel>(object); + if (graph) { + add_graph(graph); + } +} + +void +GraphTreeWindow::add_graph(SPtr<GraphModel> pm) +{ + if (!pm->parent()) { + Gtk::TreeModel::iterator iter = _graph_treestore->append(); + Gtk::TreeModel::Row row = *iter; + if (pm->path().is_root()) { + row[_graph_tree_columns.name_col] = _app->interface()->uri().string(); + } else { + row[_graph_tree_columns.name_col] = pm->symbol().c_str(); + } + row[_graph_tree_columns.enabled_col] = pm->enabled(); + row[_graph_tree_columns.graph_model_col] = pm; + _graphs_treeview->expand_row(_graph_treestore->get_path(iter), true); + } else { + Gtk::TreeModel::Children children = _graph_treestore->children(); + Gtk::TreeModel::iterator c = find_graph(children, pm->parent()); + + if (c != children.end()) { + Gtk::TreeModel::iterator iter = _graph_treestore->append(c->children()); + Gtk::TreeModel::Row row = *iter; + row[_graph_tree_columns.name_col] = pm->symbol().c_str(); + row[_graph_tree_columns.enabled_col] = pm->enabled(); + row[_graph_tree_columns.graph_model_col] = pm; + _graphs_treeview->expand_row(_graph_treestore->get_path(iter), true); + } + } + + pm->signal_property().connect( + sigc::bind(sigc::mem_fun(this, &GraphTreeWindow::graph_property_changed), + pm)); + + pm->signal_moved().connect( + sigc::bind(sigc::mem_fun(this, &GraphTreeWindow::graph_moved), + pm)); + + pm->signal_destroyed().connect( + sigc::bind(sigc::mem_fun(this, &GraphTreeWindow::remove_graph), + pm)); +} + +void +GraphTreeWindow::remove_graph(SPtr<GraphModel> pm) +{ + Gtk::TreeModel::iterator i = find_graph(_graph_treestore->children(), pm); + if (i != _graph_treestore->children().end()) { + _graph_treestore->erase(i); + } +} + +Gtk::TreeModel::iterator +GraphTreeWindow::find_graph(Gtk::TreeModel::Children root, + SPtr<Client::ObjectModel> graph) +{ + for (Gtk::TreeModel::iterator c = root.begin(); c != root.end(); ++c) { + SPtr<GraphModel> pm = (*c)[_graph_tree_columns.graph_model_col]; + if (graph == pm) { + return c; + } else if ((*c)->children().size() > 0) { + Gtk::TreeModel::iterator ret = find_graph(c->children(), graph); + if (ret != c->children().end()) { + return ret; + } + } + } + return root.end(); +} + +/** Show the context menu for the selected graph in the graphs treeview. + */ +void +GraphTreeWindow::show_graph_menu(GdkEventButton* ev) +{ + Gtk::TreeModel::iterator active = _graph_tree_selection->get_selected(); + if (active) { + Gtk::TreeModel::Row row = *active; + SPtr<GraphModel> pm = row[_graph_tree_columns.graph_model_col]; + if (pm) { + _app->log().warn("TODO: graph menu from tree window"); + } + } +} + +void +GraphTreeWindow::event_graph_activated(const Gtk::TreeModel::Path& path, + Gtk::TreeView::Column* col) +{ + Gtk::TreeModel::iterator active = _graph_treestore->get_iter(path); + Gtk::TreeModel::Row row = *active; + SPtr<GraphModel> pm = row[_graph_tree_columns.graph_model_col]; + + _app->window_factory()->present_graph(pm); +} + +void +GraphTreeWindow::event_graph_enabled_toggled(const Glib::ustring& path_str) +{ + Gtk::TreeModel::Path path(path_str); + Gtk::TreeModel::iterator active = _graph_treestore->get_iter(path); + Gtk::TreeModel::Row row = *active; + + SPtr<GraphModel> pm = row[_graph_tree_columns.graph_model_col]; + assert(pm); + + if (_enable_signal) { + _app->set_property(pm->uri(), + _app->uris().ingen_enabled, + _app->forge().make((bool)!pm->enabled())); + } +} + +void +GraphTreeWindow::graph_property_changed(const URI& key, + const Atom& value, + SPtr<GraphModel> graph) +{ + const URIs& uris = _app->uris(); + _enable_signal = false; + if (key == uris.ingen_enabled && value.type() == uris.forge.Bool) { + Gtk::TreeModel::iterator i = find_graph(_graph_treestore->children(), graph); + if (i != _graph_treestore->children().end()) { + Gtk::TreeModel::Row row = *i; + row[_graph_tree_columns.enabled_col] = value.get<int32_t>(); + } else { + _app->log().error(fmt("Unable to find graph %1%\n") + % graph->path()); + } + } + _enable_signal = true; +} + +void +GraphTreeWindow::graph_moved(SPtr<GraphModel> graph) +{ + _enable_signal = false; + + Gtk::TreeModel::iterator i + = find_graph(_graph_treestore->children(), graph); + + if (i != _graph_treestore->children().end()) { + Gtk::TreeModel::Row row = *i; + row[_graph_tree_columns.name_col] = graph->symbol().c_str(); + } else { + _app->log().error(fmt("Unable to find graph %1%\n") + % graph->path()); + } + + _enable_signal = true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphTreeWindow.hpp b/src/gui/GraphTreeWindow.hpp new file mode 100644 index 00000000..005f39a8 --- /dev/null +++ b/src/gui/GraphTreeWindow.hpp @@ -0,0 +1,123 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPHTREEWINDOW_HPP +#define INGEN_GUI_GRAPHTREEWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> + +#include "Window.hpp" + +namespace Raul { class Path; } + +namespace Ingen { + +namespace Client { class ClientStore; class ObjectModel; } + +namespace GUI { + +class GraphWindow; +class GraphTreeView; + +/** Window with a TreeView of all loaded graphs. + * + * \ingroup GUI + */ +class GraphTreeWindow : public Window +{ +public: + GraphTreeWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app, Client::ClientStore& store); + + void new_object(SPtr<Client::ObjectModel> object); + + void graph_property_changed(const URI& key, + const Atom& value, + SPtr<Client::GraphModel> graph); + + void graph_moved(SPtr<Client::GraphModel> graph); + + void add_graph(SPtr<Client::GraphModel> pm); + void remove_graph(SPtr<Client::GraphModel> pm); + void show_graph_menu(GdkEventButton* ev); + +protected: + void event_graph_activated(const Gtk::TreeModel::Path& path, + Gtk::TreeView::Column* col); + + void event_graph_enabled_toggled(const Glib::ustring& path_str); + + Gtk::TreeModel::iterator find_graph( + Gtk::TreeModel::Children root, + SPtr<Client::ObjectModel> graph); + + GraphTreeView* _graphs_treeview; + + struct GraphTreeModelColumns : public Gtk::TreeModel::ColumnRecord + { + GraphTreeModelColumns() { + add(name_col); + add(enabled_col); + add(graph_model_col); + } + + Gtk::TreeModelColumn<Glib::ustring> name_col; + Gtk::TreeModelColumn<bool> enabled_col; + Gtk::TreeModelColumn<SPtr<Client::GraphModel> > graph_model_col; + }; + + App* _app; + GraphTreeModelColumns _graph_tree_columns; + Glib::RefPtr<Gtk::TreeStore> _graph_treestore; + Glib::RefPtr<Gtk::TreeSelection> _graph_tree_selection; + bool _enable_signal; +}; + +/** Derived TreeView class to support context menus for graphs */ +class GraphTreeView : public Gtk::TreeView +{ +public: + GraphTreeView(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::TreeView(cobject) + , _window(NULL) + {} + + void set_window(GraphTreeWindow* win) { _window = win; } + + bool on_button_press_event(GdkEventButton* ev) { + bool ret = Gtk::TreeView::on_button_press_event(ev); + + if ((ev->type == GDK_BUTTON_PRESS) && (ev->button == 3)) + _window->show_graph_menu(ev); + + return ret; + } + +private: + GraphTreeWindow* _window; + +}; // struct GraphTreeView + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPHTREEWINDOW_HPP diff --git a/src/gui/GraphView.cpp b/src/gui/GraphView.cpp new file mode 100644 index 00000000..e6361249 --- /dev/null +++ b/src/gui/GraphView.cpp @@ -0,0 +1,154 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <fstream> + +#include "ingen/Interface.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "LoadPluginWindow.hpp" +#include "NewSubgraphWindow.hpp" +#include "GraphCanvas.hpp" +#include "GraphTreeWindow.hpp" +#include "GraphView.hpp" +#include "WidgetFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +GraphView::GraphView(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::Box(cobject) + , _app(nullptr) + , _breadcrumb_container(nullptr) + , _enable_signal(true) +{ + property_visible() = false; + + xml->get_widget("graph_view_breadcrumb_container", _breadcrumb_container); + xml->get_widget("graph_view_toolbar", _toolbar); + xml->get_widget("graph_view_process_but", _process_but); + xml->get_widget("graph_view_poly_spin", _poly_spin); + xml->get_widget("graph_view_scrolledwindow", _canvas_scrolledwindow); + + _toolbar->set_toolbar_style(Gtk::TOOLBAR_ICONS); + _canvas_scrolledwindow->property_hadjustment().get_value()->set_step_increment(10); + _canvas_scrolledwindow->property_vadjustment().get_value()->set_step_increment(10); +} + +GraphView::~GraphView() +{ + _canvas_scrolledwindow->remove(); +} + +void +GraphView::init(App& app) +{ + _app = &app; +} + +void +GraphView::set_graph(SPtr<const GraphModel> graph) +{ + assert(!_canvas); // FIXME: remove + + assert(_breadcrumb_container); // ensure created + + _graph = graph; + _canvas = SPtr<GraphCanvas>(new GraphCanvas(*_app, graph, 1600*2, 1200*2)); + _canvas->build(); + + _canvas_scrolledwindow->add(_canvas->widget()); + + _poly_spin->set_range(1, 128); + _poly_spin->set_increments(1, 4); + _poly_spin->set_value(graph->internal_poly()); + + for (const auto& p : graph->properties()) { + property_changed(p.first, p.second); + } + + // Connect model signals to track state + graph->signal_property().connect( + sigc::mem_fun(this, &GraphView::property_changed)); + + // Connect widget signals to do things + _process_but->signal_toggled().connect( + sigc::mem_fun(this, &GraphView::process_toggled)); + + _poly_spin->signal_value_changed().connect( + sigc::mem_fun(*this, &GraphView::poly_changed)); + + _canvas->widget().grab_focus(); +} + +SPtr<GraphView> +GraphView::create(App& app, SPtr<const GraphModel> graph) +{ + GraphView* result = nullptr; + Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create("warehouse_win"); + xml->get_widget_derived("graph_view_box", result); + result->init(app); + result->set_graph(graph); + return SPtr<GraphView>(result); +} + +void +GraphView::process_toggled() +{ + if (!_enable_signal) { + return; + } + + _app->set_property(_graph->uri(), + _app->uris().ingen_enabled, + _app->forge().make((bool)_process_but->get_active())); +} + +void +GraphView::poly_changed() +{ + const int poly = _poly_spin->get_value_as_int(); + if (_enable_signal && poly != (int)_graph->internal_poly()) { + _app->set_property(_graph->uri(), + _app->uris().ingen_polyphony, + _app->forge().make(poly)); + } +} + +void +GraphView::property_changed(const URI& predicate, const Atom& value) +{ + _enable_signal = false; + if (predicate == _app->uris().ingen_enabled) { + if (value.type() == _app->uris().forge.Bool) { + _process_but->set_active(value.get<int32_t>()); + } + } else if (predicate == _app->uris().ingen_polyphony) { + if (value.type() == _app->uris().forge.Int) { + _poly_spin->set_value(value.get<int32_t>()); + } + } + _enable_signal = true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphView.hpp b/src/gui/GraphView.hpp new file mode 100644 index 00000000..03569831 --- /dev/null +++ b/src/gui/GraphView.hpp @@ -0,0 +1,98 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPHVIEW_HPP +#define INGEN_GUI_GRAPHVIEW_HPP + +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/toggletoolbutton.h> +#include <gtkmm/toolbar.h> +#include <gtkmm/toolitem.h> +#include <gtkmm/toolitem.h> + +#include "ingen/types.hpp" + +namespace Raul { class Atom; } + +namespace Ingen { + +namespace Client { +class PortModel; +class MetadataModel; +class GraphModel; +class ObjectModel; +} + +namespace GUI { + +class App; +class LoadPluginWindow; +class NewSubgraphWindow; +class GraphCanvas; +class GraphDescriptionWindow; +class SubgraphModule; + +/** The graph specific contents of a GraphWindow (ie the canvas and whatever else). + * + * \ingroup GUI + */ +class GraphView : public Gtk::Box +{ +public: + GraphView(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + ~GraphView(); + + void init(App& app); + + SPtr<GraphCanvas> canvas() const { return _canvas; } + SPtr<const Client::GraphModel> graph() const { return _graph; } + Gtk::ToolItem* breadcrumb_container() const { return _breadcrumb_container; } + + static SPtr<GraphView> create(App& app, + SPtr<const Client::GraphModel> graph); + +private: + void set_graph(SPtr<const Client::GraphModel> graph); + + void process_toggled(); + void poly_changed(); + void clear_clicked(); + + void property_changed(const URI& predicate, const Atom& value); + + App* _app; + + SPtr<const Client::GraphModel> _graph; + SPtr<GraphCanvas> _canvas; + + Gtk::ScrolledWindow* _canvas_scrolledwindow; + Gtk::Toolbar* _toolbar; + Gtk::ToggleToolButton* _process_but; + Gtk::SpinButton* _poly_spin; + Gtk::ToolItem* _breadcrumb_container; + + bool _enable_signal; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPHVIEW_HPP diff --git a/src/gui/GraphWindow.cpp b/src/gui/GraphWindow.cpp new file mode 100644 index 00000000..b5a89c79 --- /dev/null +++ b/src/gui/GraphWindow.cpp @@ -0,0 +1,85 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "GraphCanvas.hpp" +#include "GraphView.hpp" +#include "GraphWindow.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { +namespace GUI { + +GraphWindow::GraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) + , _box(nullptr) + , _position_stored(false) + , _x(0) + , _y(0) +{ + property_visible() = false; + + xml->get_widget_derived("graph_win_vbox", _box); + + set_title("Ingen"); +} + +GraphWindow::~GraphWindow() +{ + delete _box; +} + +void +GraphWindow::init_window(App& app) +{ + Window::init_window(app); + _box->init_box(app); + _box->set_window(this); +} + +void +GraphWindow::on_show() +{ + if (_position_stored) { + move(_x, _y); + } + + Gtk::Window::on_show(); + + _box->view()->canvas()->widget().grab_focus(); +} + +void +GraphWindow::on_hide() +{ + _position_stored = true; + get_position(_x, _y); + Gtk::Window::on_hide(); +} + +bool +GraphWindow::on_key_press_event(GdkEventKey* event) +{ + // Disable Window C-w handling so quit works correctly + return Gtk::Window::on_key_press_event(event); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphWindow.hpp b/src/gui/GraphWindow.hpp new file mode 100644 index 00000000..b4e51d7b --- /dev/null +++ b/src/gui/GraphWindow.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPH_WINDOW_HPP +#define INGEN_GUI_GRAPH_WINDOW_HPP + +#include <string> + +#include <gtkmm/builder.h> + +#include "ingen/types.hpp" + +#include "GraphBox.hpp" +#include "Window.hpp" + +namespace Ingen { + +namespace Client { +class GraphModel; +} + +namespace GUI { + +/** A window for a graph. + * + * \ingroup GUI + */ +class GraphWindow : public Window +{ +public: + GraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + ~GraphWindow(); + + void init_window(App& app); + + SPtr<const Client::GraphModel> graph() const { return _box->graph(); } + GraphBox* box() const { return _box; } + + bool documentation_is_visible() { return _box->documentation_is_visible(); } + + void set_documentation(const std::string& doc, bool html) { + _box->set_documentation(doc, html); + } + + void show_port_status(const Client::PortModel* model, + const Atom& value) { + _box->show_port_status(model, value); + } + +protected: + void on_hide(); + void on_show(); + bool on_key_press_event(GdkEventKey* event); + +private: + GraphBox* _box; + bool _position_stored; + int _x; + int _y; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPH_WINDOW_HPP diff --git a/src/gui/LoadGraphWindow.cpp b/src/gui/LoadGraphWindow.cpp new file mode 100644 index 00000000..b02ca510 --- /dev/null +++ b/src/gui/LoadGraphWindow.cpp @@ -0,0 +1,257 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <list> +#include <string> + +#include <boost/optional.hpp> +#include <glibmm/miscutils.h> + +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/runtime_paths.hpp" + +#include "App.hpp" +#include "GraphView.hpp" +#include "LoadGraphWindow.hpp" +#include "Style.hpp" +#include "ThreadedLoader.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +LoadGraphWindow::LoadGraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::FileChooserDialog(cobject) + , _app(nullptr) + , _merge_ports(false) +{ + xml->get_widget("load_graph_symbol_label", _symbol_label); + xml->get_widget("load_graph_symbol_entry", _symbol_entry); + xml->get_widget("load_graph_ports_label", _ports_label); + xml->get_widget("load_graph_merge_ports_radio", _merge_ports_radio); + xml->get_widget("load_graph_insert_ports_radio", _insert_ports_radio); + xml->get_widget("load_graph_poly_voices_radio", _poly_voices_radio); + xml->get_widget("load_graph_poly_from_file_radio", _poly_from_file_radio); + xml->get_widget("load_graph_poly_spinbutton", _poly_spinbutton); + xml->get_widget("load_graph_ok_button", _ok_button); + xml->get_widget("load_graph_cancel_button", _cancel_button); + + _cancel_button->signal_clicked().connect( + sigc::mem_fun(this, &LoadGraphWindow::cancel_clicked)); + _ok_button->signal_clicked().connect( + sigc::mem_fun(this, &LoadGraphWindow::ok_clicked)); + _merge_ports_radio->signal_toggled().connect( + sigc::mem_fun(this, &LoadGraphWindow::merge_ports_selected)); + _insert_ports_radio->signal_toggled().connect( + sigc::mem_fun(this, &LoadGraphWindow::insert_ports_selected)); + _poly_from_file_radio->signal_toggled().connect( + sigc::bind(sigc::mem_fun(_poly_spinbutton, &Gtk::SpinButton::set_sensitive), + false)); + _poly_voices_radio->signal_toggled().connect( + sigc::bind(sigc::mem_fun(_poly_spinbutton, &Gtk::SpinButton::set_sensitive), + true)); + + signal_selection_changed().connect( + sigc::mem_fun(this, &LoadGraphWindow::selection_changed)); + + Gtk::FileFilter file_filter; + file_filter.add_pattern("*.ttl"); + file_filter.set_name("Ingen graph files (*.ttl)"); + add_filter(file_filter); + + Gtk::FileFilter bundle_filter; + bundle_filter.add_pattern("*.ingen"); + bundle_filter.set_name("Ingen bundles (*.ingen)"); + add_filter(bundle_filter); + + property_select_multiple() = true; + + // Add global examples directory to "shortcut folders" (bookmarks) + const FilePath examples_dir = Ingen::data_file_path("graphs"); + if (Glib::file_test(examples_dir, Glib::FILE_TEST_IS_DIR)) { + add_shortcut_folder(examples_dir.string()); + } +} + +void +LoadGraphWindow::present(SPtr<const GraphModel> graph, + bool import, + Properties data) +{ + _import = import; + set_graph(graph); + _symbol_label->property_visible() = !import; + _symbol_entry->property_visible() = !import; + _ports_label->property_visible() = _import; + _merge_ports_radio->property_visible() = _import; + _insert_ports_radio->property_visible() = _import; + _initial_data = data; + Gtk::Window::present(); +} + +/** Sets the graph model for this window and initializes everything. + * + * This function MUST be called before using the window in any way! + */ +void +LoadGraphWindow::set_graph(SPtr<const GraphModel> graph) +{ + _graph = graph; + _symbol_entry->set_text(""); + _symbol_entry->set_sensitive(!_import); + _poly_spinbutton->set_value(graph->internal_poly()); +} + +void +LoadGraphWindow::on_show() +{ + const Atom& dir = _app->world()->conf().option("graph-directory"); + if (dir.is_valid()) { + set_current_folder(dir.ptr<char>()); + } + Gtk::FileChooserDialog::on_show(); +} + +void +LoadGraphWindow::merge_ports_selected() +{ + _merge_ports = true; +} + +void +LoadGraphWindow::insert_ports_selected() +{ + _merge_ports = false; +} + +void +LoadGraphWindow::ok_clicked() +{ + if (!_graph) { + hide(); + return; + } + + const URIs& uris = _app->uris(); + + if (_poly_voices_radio->get_active()) { + _initial_data.emplace( + uris.ingen_polyphony, + _app->forge().make(_poly_spinbutton->get_value_as_int())); + } + + if (get_uri() == "") { + return; + } + + if (_import) { + // If unset load_graph will load value + boost::optional<Raul::Path> parent; + boost::optional<Raul::Symbol> symbol; + if (!_graph->path().is_root()) { + parent = _graph->path().parent(); + symbol = _graph->symbol(); + } + + _app->loader()->load_graph( + true, FilePath(get_filename()), parent, symbol, _initial_data); + + } else { + std::list<Glib::ustring> uri_list = get_filenames(); + for (auto u : uri_list) { + // Cascade + Atom& x = _initial_data.find(uris.ingen_canvasX)->second; + x = _app->forge().make(x.get<float>() + 20.0f); + Atom& y = _initial_data.find(uris.ingen_canvasY)->second; + y = _app->forge().make(y.get<float>() + 20.0f); + + Raul::Symbol symbol(symbol_from_filename(u)); + if (uri_list.size() == 1 && _symbol_entry->get_text() != "") { + symbol = Raul::Symbol::symbolify(_symbol_entry->get_text()); + } + + symbol = avoid_symbol_clash(symbol); + + _app->loader()->load_graph( + false, FilePath(URI(u).path()), _graph->path(), symbol, _initial_data); + } + } + + _graph.reset(); + hide(); + + _app->world()->conf().set( + "graph-directory", + _app->world()->forge().alloc(get_current_folder())); +} + +void +LoadGraphWindow::cancel_clicked() +{ + _graph.reset(); + hide(); +} + +Raul::Symbol +LoadGraphWindow::symbol_from_filename(const Glib::ustring& filename) +{ + std::string symbol_str = Glib::path_get_basename(get_filename()); + symbol_str = symbol_str.substr(0, symbol_str.find('.')); + return Raul::Symbol::symbolify(symbol_str); +} + +Raul::Symbol +LoadGraphWindow::avoid_symbol_clash(const Raul::Symbol& symbol) +{ + unsigned offset = _app->store()->child_name_offset( + _graph->path(), symbol); + + if (offset != 0) { + std::stringstream ss; + ss << symbol << "_" << offset; + return Raul::Symbol(ss.str()); + } else { + return symbol; + } +} + +void +LoadGraphWindow::selection_changed() +{ + if (_import) { + return; + } + + if (get_filenames().size() != 1) { + _symbol_entry->set_text(""); + _symbol_entry->set_sensitive(false); + } else { + _symbol_entry->set_text( + avoid_symbol_clash(symbol_from_filename(get_filename())).c_str()); + _symbol_entry->set_sensitive(true); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/LoadGraphWindow.hpp b/src/gui/LoadGraphWindow.hpp new file mode 100644 index 00000000..8ec5ed4b --- /dev/null +++ b/src/gui/LoadGraphWindow.hpp @@ -0,0 +1,95 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_LOADGRAPHWINDOW_HPP +#define INGEN_GUI_LOADGRAPHWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/filechooserdialog.h> +#include <gtkmm/label.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/spinbutton.h> + +#include "ingen/Node.hpp" +#include "ingen/types.hpp" + +namespace Ingen { + +namespace Client { class GraphModel; } + +namespace GUI { + +/** 'Load Graph' Window. + * + * Loaded from XML as a derived object. + * + * \ingroup GUI + */ +class LoadGraphWindow : public Gtk::FileChooserDialog +{ +public: + LoadGraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app) { _app = &app; } + + void set_graph(SPtr<const Client::GraphModel> graph); + + void present(SPtr<const Client::GraphModel> graph, + bool import, + Properties data); + +protected: + void on_show(); + +private: + void merge_ports_selected(); + void insert_ports_selected(); + + void selection_changed(); + void cancel_clicked(); + void ok_clicked(); + + Raul::Symbol symbol_from_filename(const Glib::ustring& filename); + Raul::Symbol avoid_symbol_clash(const Raul::Symbol& symbol); + + App* _app; + + Properties _initial_data; + + SPtr<const Client::GraphModel> _graph; + + Gtk::Label* _symbol_label; + Gtk::Entry* _symbol_entry; + Gtk::Label* _ports_label; + Gtk::RadioButton* _merge_ports_radio; + Gtk::RadioButton* _insert_ports_radio; + Gtk::RadioButton* _poly_voices_radio; + Gtk::RadioButton* _poly_from_file_radio; + Gtk::SpinButton* _poly_spinbutton; + Gtk::Button* _ok_button; + Gtk::Button* _cancel_button; + + bool _import; + bool _merge_ports; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_LOADGRAPHWINDOW_HPP diff --git a/src/gui/LoadPluginWindow.cpp b/src/gui/LoadPluginWindow.cpp new file mode 100644 index 00000000..c96634cc --- /dev/null +++ b/src/gui/LoadPluginWindow.cpp @@ -0,0 +1,515 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include <stddef.h> + +#include <cassert> +#include <algorithm> + +#include "ingen/Interface.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "LoadPluginWindow.hpp" +#include "GraphCanvas.hpp" +#include "GraphView.hpp" +#include "GraphWindow.hpp" + +#include "ingen_config.h" + +using std::string; + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +LoadPluginWindow::LoadPluginWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) + , _name_offset(0) + , _has_shown(false) + , _refresh_list(true) +{ + xml->get_widget("load_plugin_plugins_treeview", _plugins_treeview); + xml->get_widget("load_plugin_polyphonic_checkbutton", _polyphonic_checkbutton); + xml->get_widget("load_plugin_name_entry", _name_entry); + xml->get_widget("load_plugin_add_button", _add_button); + xml->get_widget("load_plugin_close_button", _close_button); + + xml->get_widget("load_plugin_filter_combo", _filter_combo); + xml->get_widget("load_plugin_search_entry", _search_entry); + + // Set up the plugins list + _plugins_liststore = Gtk::ListStore::create(_plugins_columns); + _plugins_treeview->set_model(_plugins_liststore); + _plugins_treeview->append_column("_Name", _plugins_columns._col_name); + _plugins_treeview->append_column("_Type", _plugins_columns._col_type); + _plugins_treeview->append_column("_Project", _plugins_columns._col_project); + _plugins_treeview->append_column("_Author", _plugins_columns._col_author); + _plugins_treeview->append_column("_URI", _plugins_columns._col_uri); + + // This could be nicer.. store the TreeViewColumns locally maybe? + _plugins_treeview->get_column(0)->set_sort_column(_plugins_columns._col_name); + _plugins_treeview->get_column(1)->set_sort_column(_plugins_columns._col_type); + _plugins_treeview->get_column(2)->set_sort_column(_plugins_columns._col_project); + _plugins_treeview->get_column(2)->set_sort_column(_plugins_columns._col_author); + _plugins_treeview->get_column(3)->set_sort_column(_plugins_columns._col_uri); + for (int i = 0; i < 5; ++i) { + _plugins_treeview->get_column(i)->set_resizable(true); + } + + // Set up the search criteria combobox + _criteria_liststore = Gtk::ListStore::create(_criteria_columns); + _filter_combo->set_model(_criteria_liststore); + + Gtk::TreeModel::iterator iter = _criteria_liststore->append(); + Gtk::TreeModel::Row row = *iter; + row[_criteria_columns._col_label] = "Name contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::NAME; + _filter_combo->set_active(iter); + + row = *(iter = _criteria_liststore->append()); + row[_criteria_columns._col_label] = "Type contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::TYPE; + + row = *(iter = _criteria_liststore->append()); + row[_criteria_columns._col_label] = "Project contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::PROJECT; + + row = *(iter = _criteria_liststore->append()); + row[_criteria_columns._col_label] = "Author contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::AUTHOR; + + row = *(iter = _criteria_liststore->append()); + row[_criteria_columns._col_label] = "URI contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::URI; + _filter_combo->pack_start(_criteria_columns._col_label); + + _add_button->signal_clicked().connect( + sigc::mem_fun(this, &LoadPluginWindow::add_clicked)); + _close_button->signal_clicked().connect( + sigc::mem_fun(this, &Window::hide)); + _plugins_treeview->signal_row_activated().connect( + sigc::mem_fun(this, &LoadPluginWindow::plugin_activated)); + _search_entry->signal_activate().connect( + sigc::mem_fun(this, &LoadPluginWindow::add_clicked)); + _search_entry->signal_changed().connect( + sigc::mem_fun(this, &LoadPluginWindow::filter_changed)); + _name_entry->signal_changed().connect( + sigc::mem_fun(this, &LoadPluginWindow::name_changed)); + +#ifdef HAVE_NEW_GTKMM + _search_entry->signal_icon_release().connect( + sigc::mem_fun(this, &LoadPluginWindow::name_cleared)); +#endif + + _selection = _plugins_treeview->get_selection(); + _selection->set_mode(Gtk::SELECTION_MULTIPLE); + _selection->signal_changed().connect( + sigc::mem_fun(this, &LoadPluginWindow::plugin_selection_changed)); + + //m_add_button->grab_default(); +} + +void +LoadPluginWindow::present(SPtr<const GraphModel> graph, + Properties data) +{ + set_graph(graph); + _initial_data = data; + Gtk::Window::present(); +} + +/** Called every time the user types into the name input box. + * Used to display warning messages, and enable/disable the OK button. + */ +void +LoadPluginWindow::name_changed() +{ + // Toggle add button sensitivity according name legality + if (_selection->get_selected_rows().size() == 1) { + const string sym = _name_entry->get_text(); + if (!Raul::Symbol::is_valid(sym)) { + _add_button->property_sensitive() = false; + } else if (_app->store()->find(_graph->path().child(Raul::Symbol(sym))) + != _app->store()->end()) { + _add_button->property_sensitive() = false; + } else { + _add_button->property_sensitive() = true; + } + } +} + +#ifdef HAVE_NEW_GTKMM +void +LoadPluginWindow::name_cleared(Gtk::EntryIconPosition pos, const GdkEventButton* event) +{ + _search_entry->set_text(""); +} +#endif // HAVE_NEW_GTKMM + +/** Sets the graph controller for this window and initializes everything. + * + * This function MUST be called before using the window in any way! + */ +void +LoadPluginWindow::set_graph(SPtr<const GraphModel> graph) +{ + if (_graph) { + _graph = graph; + plugin_selection_changed(); + } else { + _graph = graph; + } +} + +/** Populates the plugin list on the first show. + * + * This is done here instead of construction time as the list population is + * really expensive and bogs down creation of a graph. This is especially + * important when many graph notifications are sent at one time from the + * engine. + */ +void +LoadPluginWindow::on_show() +{ + if (!_has_shown) { + _app->store()->signal_new_plugin().connect( + sigc::mem_fun(this, &LoadPluginWindow::add_plugin)); + _has_shown = true; + } + + if (_refresh_list) { + set_plugins(_app->store()->plugins()); + _refresh_list = false; + } + + Gtk::Window::on_show(); +} + +void +LoadPluginWindow::set_plugins(SPtr<const ClientStore::Plugins> plugins) +{ + _rows.clear(); + _plugins_liststore->clear(); + + for (const auto& p : *plugins.get()) { + add_plugin(p.second); + } + + _plugins_liststore->set_sort_column(1, Gtk::SORT_ASCENDING); + _plugins_treeview->columns_autosize(); +} + +void +LoadPluginWindow::new_plugin(SPtr<const PluginModel> pm) +{ + if (is_visible()) { + add_plugin(pm); + } else { + _refresh_list = true; + } +} + +static std::string +get_project_name(SPtr<const PluginModel> plugin) +{ + std::string name; + if (plugin->lilv_plugin()) { + LilvNode* project = lilv_plugin_get_project(plugin->lilv_plugin()); + if (!project) { + return ""; + } + + LilvNode* doap_name = lilv_new_uri( + plugin->lilv_world(), "http://usefulinc.com/ns/doap#name"); + LilvNodes* names = lilv_world_find_nodes( + plugin->lilv_world(), project, doap_name, nullptr); + + if (names) { + name = lilv_node_as_string(lilv_nodes_get_first(names)); + } + + lilv_nodes_free(names); + lilv_node_free(doap_name); + lilv_node_free(project); + } + return name; +} + +static std::string +get_author_name(SPtr<const PluginModel> plugin) +{ + std::string name; + if (plugin->lilv_plugin()) { + LilvNode* author = lilv_plugin_get_author_name(plugin->lilv_plugin()); + if (author) { + name = lilv_node_as_string(author); + } + lilv_node_free(author); + } + return name; +} + +void +LoadPluginWindow::set_row(Gtk::TreeModel::Row& row, + SPtr<const PluginModel> plugin) +{ + const URIs& uris = _app->uris(); + const Atom& name = plugin->get_property(uris.doap_name); + if (name.is_valid() && name.type() == uris.forge.String) { + row[_plugins_columns._col_name] = name.ptr<char>(); + } + + if (uris.lv2_Plugin == plugin->type()) { + row[_plugins_columns._col_type] = lilv_node_as_string( + lilv_plugin_class_get_label( + lilv_plugin_get_class(plugin->lilv_plugin()))); + + row[_plugins_columns._col_project] = get_project_name(plugin); + row[_plugins_columns._col_author] = get_author_name(plugin); + } else if (uris.ingen_Internal == plugin->type()) { + row[_plugins_columns._col_type] = "Internal"; + row[_plugins_columns._col_project] = "Ingen"; + row[_plugins_columns._col_author] = "David Robillard"; + } else if (uris.ingen_Graph == plugin->type()) { + row[_plugins_columns._col_type] = "Graph"; + } else { + row[_plugins_columns._col_type] = ""; + } + + row[_plugins_columns._col_uri] = plugin->uri().string(); + row[_plugins_columns._col_plugin] = plugin; +} + +void +LoadPluginWindow::add_plugin(SPtr<const PluginModel> plugin) +{ + if (plugin->lilv_plugin() && lilv_plugin_is_replaced(plugin->lilv_plugin())) { + return; + } + + Gtk::TreeModel::iterator iter = _plugins_liststore->append(); + Gtk::TreeModel::Row row = *iter; + _rows.emplace(plugin->uri(), iter); + + set_row(row, plugin); + + plugin->signal_property().connect( + sigc::bind<0>(sigc::mem_fun(this, &LoadPluginWindow::plugin_property_changed), + plugin->uri())); +} + +///// Event Handlers ////// + +void +LoadPluginWindow::plugin_activated(const Gtk::TreeModel::Path& path, + Gtk::TreeViewColumn* col) +{ + add_clicked(); +} + +void +LoadPluginWindow::plugin_selection_changed() +{ + size_t n_selected = _selection->get_selected_rows().size(); + if (n_selected == 0) { + _name_offset = 0; + _name_entry->set_text(""); + _name_entry->set_sensitive(false); + } else if (n_selected == 1) { + Gtk::TreeModel::iterator iter = _plugins_liststore->get_iter( + *_selection->get_selected_rows().begin()); + if (iter) { + Gtk::TreeModel::Row row = *iter; + SPtr<const PluginModel> p = row.get_value( + _plugins_columns._col_plugin); + _name_offset = _app->store()->child_name_offset( + _graph->path(), p->default_block_symbol()); + _name_entry->set_text(generate_module_name(p, _name_offset)); + _name_entry->set_sensitive(true); + } else { + _name_offset = 0; + _name_entry->set_text(""); + _name_entry->set_sensitive(false); + } + } else { + _name_entry->set_text(""); + _name_entry->set_sensitive(false); + } +} + +/** Generate an automatic name for this Node. + * + * Offset is an offset of the number that will be appended to the plugin's + * label, needed if the user adds multiple plugins faster than the engine + * sends the notification back. + */ +string +LoadPluginWindow::generate_module_name(SPtr<const PluginModel> plugin, + int offset) +{ + std::stringstream ss; + ss << plugin->default_block_symbol(); + if (offset != 0) { + ss << "_" << offset; + } + return ss.str(); +} + +void +LoadPluginWindow::load_plugin(const Gtk::TreeModel::iterator& iter) +{ + const URIs& uris = _app->uris(); + Gtk::TreeModel::Row row = *iter; + SPtr<const PluginModel> plugin = row.get_value(_plugins_columns._col_plugin); + bool polyphonic = _polyphonic_checkbutton->get_active(); + string name = _name_entry->get_text(); + + if (name.empty()) { + name = generate_module_name(plugin, _name_offset); + } + + if (name.empty() || !Raul::Symbol::is_valid(name)) { + Gtk::MessageDialog dialog( + *this, + "Unable to choose a default name, please provide one", + false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + + dialog.run(); + } else { + Raul::Path path = _graph->path().child(Raul::Symbol::symbolify(name)); + Properties props = _initial_data; + props.emplace(uris.rdf_type, Property(uris.ingen_Block)); + props.emplace(uris.lv2_prototype, _app->forge().make_urid(plugin->uri())); + props.emplace(uris.ingen_polyphonic, _app->forge().make(polyphonic)); + _app->interface()->put(path_to_uri(path), props); + + if (_selection->get_selected_rows().size() == 1) { + _name_offset = (_name_offset == 0) ? 2 : _name_offset + 1; + _name_entry->set_text(generate_module_name(plugin, _name_offset)); + } + + // Cascade next block + Atom& x = _initial_data.find(uris.ingen_canvasX)->second; + x = _app->forge().make(x.get<float>() + 20.0f); + Atom& y = _initial_data.find(uris.ingen_canvasY)->second; + y = _app->forge().make(y.get<float>() + 20.0f); + } +} + +void +LoadPluginWindow::add_clicked() +{ + _selection->selected_foreach_iter( + sigc::mem_fun(*this, &LoadPluginWindow::load_plugin)); +} + +void +LoadPluginWindow::filter_changed() +{ + _rows.clear(); + _plugins_liststore->clear(); + string search = _search_entry->get_text(); + transform(search.begin(), search.end(), search.begin(), ::toupper); + + // Get selected criteria + const Gtk::TreeModel::Row row = *(_filter_combo->get_active()); + CriteriaColumns::Criteria criteria = row[_criteria_columns._col_criteria]; + + string field; + + Gtk::TreeModel::Row model_row; + Gtk::TreeModel::iterator model_iter; + size_t num_visible = 0; + const URIs& uris = _app->uris(); + + for (const auto& p : *_app->store()->plugins().get()) { + const SPtr<PluginModel> plugin = p.second; + const Atom& name = plugin->get_property(uris.doap_name); + + switch (criteria) { + case CriteriaColumns::Criteria::NAME: + if (name.is_valid() && name.type() == uris.forge.String) { + field = name.ptr<char>(); + } + break; + case CriteriaColumns::Criteria::TYPE: + if (plugin->lilv_plugin()) { + field = lilv_node_as_string( + lilv_plugin_class_get_label( + lilv_plugin_get_class(plugin->lilv_plugin()))); + } + break; + case CriteriaColumns::Criteria::PROJECT: + field = get_project_name(plugin); + break; + case CriteriaColumns::Criteria::AUTHOR: + field = get_author_name(plugin); + break; + case CriteriaColumns::Criteria::URI: + field = plugin->uri(); + break; + } + + transform(field.begin(), field.end(), field.begin(), ::toupper); + + if (field.find(search) != string::npos) { + model_iter = _plugins_liststore->append(); + model_row = *model_iter; + set_row(model_row, plugin); + ++num_visible; + } + } + + if (num_visible == 1) { + _selection->unselect_all(); + _selection->select(model_iter); + } +} + +bool +LoadPluginWindow::on_key_press_event(GdkEventKey* event) +{ + if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) { + hide(); + return true; + } else { + return Gtk::Window::on_key_press_event(event); + } +} + +void +LoadPluginWindow::plugin_property_changed(const URI& plugin, + const URI& predicate, + const Atom& value) +{ + const URIs& uris = _app->uris(); + if (predicate == uris.doap_name) { + Rows::const_iterator i = _rows.find(plugin); + if (i != _rows.end() && value.type() == uris.forge.String) { + (*i->second)[_plugins_columns._col_name] = value.ptr<char>(); + } + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/LoadPluginWindow.hpp b/src/gui/LoadPluginWindow.hpp new file mode 100644 index 00000000..3874b8dd --- /dev/null +++ b/src/gui/LoadPluginWindow.hpp @@ -0,0 +1,162 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_LOADPLUGINWINDOW_HPP +#define INGEN_GUI_LOADPLUGINWINDOW_HPP + +#include <map> +#include <string> + +#include <gtkmm/builder.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treeview.h> + +#include "ingen/Node.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/types.hpp" +#include "ingen_config.h" + +#include "Window.hpp" + +namespace Ingen { + +namespace Client { +class GraphModel; +class PluginModel; +} + +namespace GUI { + +/** 'Load Plugin' window. + * + * Loaded from XML as a derived object. + * + * \ingroup GUI + */ +class LoadPluginWindow : public Window +{ +public: + LoadPluginWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void set_graph(SPtr<const Client::GraphModel> graph); + void set_plugins(SPtr<const Client::ClientStore::Plugins> plugins); + + void add_plugin(SPtr<const Client::PluginModel> plugin); + + void present(SPtr<const Client::GraphModel> graph, + Properties data); + +protected: + void on_show(); + bool on_key_press_event(GdkEventKey* event); + +private: + /** Columns for the plugin list */ + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() { + add(_col_name); + add(_col_type); + add(_col_project); + add(_col_author); + add(_col_uri); + add(_col_plugin); + } + + Gtk::TreeModelColumn<Glib::ustring> _col_name; + Gtk::TreeModelColumn<Glib::ustring> _col_type; + Gtk::TreeModelColumn<Glib::ustring> _col_project; + Gtk::TreeModelColumn<Glib::ustring> _col_author; + Gtk::TreeModelColumn<Glib::ustring> _col_uri; + + // Not displayed: + Gtk::TreeModelColumn< SPtr<const Client::PluginModel> > _col_plugin; + }; + + /** Column for the filter criteria combo box. */ + class CriteriaColumns : public Gtk::TreeModel::ColumnRecord { + public: + enum class Criteria { NAME, TYPE, PROJECT, AUTHOR, URI, }; + + CriteriaColumns() { + add(_col_label); + add(_col_criteria); + } + + Gtk::TreeModelColumn<Glib::ustring> _col_label; + Gtk::TreeModelColumn<Criteria> _col_criteria; + }; + + void add_clicked(); + void filter_changed(); + void clear_clicked(); + void name_changed(); +#ifdef HAVE_NEW_GTKMM + void name_cleared(Gtk::EntryIconPosition pos, const GdkEventButton* event); +#endif + + void set_row(Gtk::TreeModel::Row& row, + SPtr<const Client::PluginModel> plugin); + + void new_plugin(SPtr<const Client::PluginModel> pm); + + void plugin_property_changed(const URI& plugin, + const URI& predicate, + const Atom& value); + + void plugin_activated(const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn* col); + void plugin_selection_changed(); + + std::string generate_module_name(SPtr<const Client::PluginModel> plugin, + int offset=0); + + void load_plugin(const Gtk::TreeModel::iterator& iter); + + Properties _initial_data; + + SPtr<const Client::GraphModel> _graph; + + typedef std::map<URI, Gtk::TreeModel::iterator> Rows; + Rows _rows; + + Glib::RefPtr<Gtk::ListStore> _plugins_liststore; + ModelColumns _plugins_columns; + + Glib::RefPtr<Gtk::ListStore> _criteria_liststore; + CriteriaColumns _criteria_columns; + + Glib::RefPtr<Gtk::TreeSelection> _selection; + + int _name_offset; // see comments for generate_plugin_name + + bool _has_shown; + bool _refresh_list; + Gtk::TreeView* _plugins_treeview; + Gtk::CheckButton* _polyphonic_checkbutton; + Gtk::Entry* _name_entry; + Gtk::Button* _close_button; + Gtk::Button* _add_button; + Gtk::ComboBox* _filter_combo; + Gtk::Entry* _search_entry; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_LOADPLUGINWINDOW_HPP diff --git a/src/gui/MessagesWindow.cpp b/src/gui/MessagesWindow.cpp new file mode 100644 index 00000000..581e732c --- /dev/null +++ b/src/gui/MessagesWindow.cpp @@ -0,0 +1,141 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/URIs.hpp" + +#include "App.hpp" +#include "MessagesWindow.hpp" + +namespace Ingen { +namespace GUI { +using std::string; + +MessagesWindow::MessagesWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) +{ + xml->get_widget("messages_textview", _textview); + xml->get_widget("messages_clear_button", _clear_button); + xml->get_widget("messages_close_button", _close_button); + + _clear_button->signal_clicked().connect(sigc::mem_fun(this, &MessagesWindow::clear_clicked)); + _close_button->signal_clicked().connect(sigc::mem_fun(this, &Window::hide)); + + for (int s = Gtk::STATE_NORMAL; s <= Gtk::STATE_INSENSITIVE; ++s) { + _textview->modify_base((Gtk::StateType)s, Gdk::Color("#000000")); + _textview->modify_text((Gtk::StateType)s, Gdk::Color("#EEEEEC")); + } +} + +void +MessagesWindow::init_window(App& app) +{ + Glib::RefPtr<Gtk::TextTag> tag = Gtk::TextTag::create(); + tag->property_foreground() = "#EF2929"; + _tags.emplace(app.uris().log_Error, tag); + _error_tag = tag; + + tag = Gtk::TextTag::create(); + tag->property_foreground() = "#FCAF3E"; + _tags.emplace(app.uris().log_Warning, tag); + + tag = Gtk::TextTag::create(); + tag->property_foreground() = "#8AE234"; + _tags.emplace(app.uris().log_Trace, tag); + + for (const auto& t : _tags) { + _textview->get_buffer()->get_tag_table()->add(t.second); + } +} + +void +MessagesWindow::post_error(const string& msg) +{ + Glib::RefPtr<Gtk::TextBuffer> text_buf = _textview->get_buffer(); + text_buf->insert_with_tag(text_buf->end(), msg, _error_tag); + text_buf->insert(text_buf->end(), "\n"); + + if (!_clear_button->is_sensitive()) { + _clear_button->set_sensitive(true); + } + + set_urgency_hint(true); + if (!is_visible()) { + present(); + } +} + +int +MessagesWindow::log(LV2_URID type, const char* fmt, va_list args) +{ + std::lock_guard<std::mutex> lock(_mutex); + +#ifdef HAVE_VASPRINTF + char* buf = nullptr; + const int len = vasprintf(&buf, fmt, args); +#else + char* buf = g_strdup_vprintf(fmt, args); + const int len = strlen(buf); +#endif + + _stream << type << ' ' << buf << '\0'; + free(buf); + + return len; +} + +void +MessagesWindow::flush() +{ + while (true) { + LV2_URID type; + std::string line; + { + std::lock_guard<std::mutex> lock(_mutex); + if (!_stream.rdbuf()->in_avail()) { + return; + } + _stream >> type; + std::getline(_stream, line, '\0'); + } + + Glib::RefPtr<Gtk::TextBuffer> text_buf = _textview->get_buffer(); + + auto t = _tags.find(type); + if (t != _tags.end()) { + text_buf->insert_with_tag(text_buf->end(), line, t->second); + } else { + text_buf->insert(text_buf->end(), line); + } + } + + if (!_clear_button->is_sensitive()) { + _clear_button->set_sensitive(true); + } +} + +void +MessagesWindow::clear_clicked() +{ + Glib::RefPtr<Gtk::TextBuffer> text_buf = _textview->get_buffer(); + text_buf->erase(text_buf->begin(), text_buf->end()); + _clear_button->set_sensitive(false); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/MessagesWindow.hpp b/src/gui/MessagesWindow.hpp new file mode 100644 index 00000000..fa9eae1d --- /dev/null +++ b/src/gui/MessagesWindow.hpp @@ -0,0 +1,70 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_MESSAGESWINDOW_HPP +#define INGEN_GUI_MESSAGESWINDOW_HPP + +#include <mutex> +#include <sstream> +#include <string> + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/textview.h> +#include "lv2/lv2plug.in/ns/ext/log/log.h" + +#include "Window.hpp" + +namespace Ingen { +namespace GUI { + +/** Messages Window. + * + * Loaded from XML as a derived object. + * This is shown when errors occur (e.g. during graph loading). + * + * \ingroup GUI + */ +class MessagesWindow : public Window +{ +public: + MessagesWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init_window(App& app); + + int log(LV2_URID type, const char* fmt, va_list args); + void flush(); + + void post_error(const std::string& msg); + +private: + void clear_clicked(); + + std::mutex _mutex; + std::stringstream _stream; + Gtk::TextView* _textview; + Gtk::Button* _clear_button; + Gtk::Button* _close_button; + + Glib::RefPtr<Gtk::TextTag> _error_tag; + std::map< LV2_URID, Glib::RefPtr<Gtk::TextTag> > _tags; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_MESSAGESWINDOW_HPP diff --git a/src/gui/NewSubgraphWindow.cpp b/src/gui/NewSubgraphWindow.cpp new file mode 100644 index 00000000..f9dc8fc4 --- /dev/null +++ b/src/gui/NewSubgraphWindow.cpp @@ -0,0 +1,119 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/Interface.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "NewSubgraphWindow.hpp" +#include "GraphView.hpp" + +namespace Ingen { +namespace GUI { + +NewSubgraphWindow::NewSubgraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) +{ + xml->get_widget("new_subgraph_name_entry", _name_entry); + xml->get_widget("new_subgraph_message_label", _message_label); + xml->get_widget("new_subgraph_polyphony_spinbutton", _poly_spinbutton); + xml->get_widget("new_subgraph_ok_button", _ok_button); + xml->get_widget("new_subgraph_cancel_button", _cancel_button); + + _name_entry->signal_changed().connect(sigc::mem_fun(this, &NewSubgraphWindow::name_changed)); + _ok_button->signal_clicked().connect(sigc::mem_fun(this, &NewSubgraphWindow::ok_clicked)); + _cancel_button->signal_clicked().connect(sigc::mem_fun(this, &NewSubgraphWindow::cancel_clicked)); + + _ok_button->property_sensitive() = false; + + _poly_spinbutton->get_adjustment()->configure(1.0, 1.0, 128, 1.0, 10.0, 0); +} + +void +NewSubgraphWindow::present(SPtr<const Client::GraphModel> graph, + Properties data) +{ + set_graph(graph); + _initial_data = data; + Gtk::Window::present(); +} + +/** Sets the graph controller for this window and initializes everything. + * + * This function MUST be called before using the window in any way! + */ +void +NewSubgraphWindow::set_graph(SPtr<const Client::GraphModel> graph) +{ + _graph = graph; +} + +/** Called every time the user types into the name input box. + * Used to display warning messages, and enable/disable the OK button. + */ +void +NewSubgraphWindow::name_changed() +{ + std::string name = _name_entry->get_text(); + if (!Raul::Symbol::is_valid(name)) { + _message_label->set_text("Name contains invalid characters."); + _ok_button->property_sensitive() = false; + } else if (_app->store()->find(_graph->path().child(Raul::Symbol(name))) + != _app->store()->end()) { + _message_label->set_text("An object already exists with that name."); + _ok_button->property_sensitive() = false; + } else { + _message_label->set_text(""); + _ok_button->property_sensitive() = true; + } +} + +void +NewSubgraphWindow::ok_clicked() +{ + const uint32_t poly = _poly_spinbutton->get_value_as_int(); + const Raul::Path path = _graph->path().child( + Raul::Symbol::symbolify(_name_entry->get_text())); + + // Create graph + Properties props; + props.emplace(_app->uris().rdf_type, Property(_app->uris().ingen_Graph)); + props.emplace(_app->uris().ingen_polyphony, _app->forge().make(int32_t(poly))); + props.emplace(_app->uris().ingen_enabled, _app->forge().make(bool(true))); + _app->interface()->put( + path_to_uri(path), props, Resource::Graph::INTERNAL); + + // Set external (block perspective) properties + props = _initial_data; + props.emplace(_app->uris().rdf_type, Property(_app->uris().ingen_Graph)); + _app->interface()->put( + path_to_uri(path), _initial_data, Resource::Graph::EXTERNAL); + + hide(); +} + +void +NewSubgraphWindow::cancel_clicked() +{ + hide(); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/NewSubgraphWindow.hpp b/src/gui/NewSubgraphWindow.hpp new file mode 100644 index 00000000..395856ba --- /dev/null +++ b/src/gui/NewSubgraphWindow.hpp @@ -0,0 +1,72 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_NEWSUBGRAPHWINDOW_HPP +#define INGEN_GUI_NEWSUBGRAPHWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/label.h> +#include <gtkmm/spinbutton.h> + +#include "ingen/Node.hpp" +#include "ingen/types.hpp" + +#include "Window.hpp" + +namespace Ingen { + +namespace Client { class GraphModel; } + +namespace GUI { + +/** 'New Subgraph' window. + * + * Loaded from XML as a derived object. + * + * \ingroup GUI + */ +class NewSubgraphWindow : public Window +{ +public: + NewSubgraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void set_graph(SPtr<const Client::GraphModel> graph); + + void present(SPtr<const Client::GraphModel> graph, + Properties data); + +private: + void name_changed(); + void ok_clicked(); + void cancel_clicked(); + + Properties _initial_data; + SPtr<const Client::GraphModel> _graph; + + Gtk::Entry* _name_entry; + Gtk::Label* _message_label; + Gtk::SpinButton* _poly_spinbutton; + Gtk::Button* _ok_button; + Gtk::Button* _cancel_button; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_NEWSUBGRAPHWINDOW_HPP diff --git a/src/gui/NodeMenu.cpp b/src/gui/NodeMenu.cpp new file mode 100644 index 00000000..1b1b1677 --- /dev/null +++ b/src/gui/NodeMenu.cpp @@ -0,0 +1,253 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include <gtkmm/entry.h> +#include <gtkmm/filechooserdialog.h> +#include <gtkmm/image.h> +#include <gtkmm/stock.h> + +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "lv2/lv2plug.in/ns/ext/presets/presets.h" + +#include "App.hpp" +#include "NodeMenu.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +NodeMenu::NodeMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : ObjectMenu(cobject, xml) + , _presets_menu(nullptr) +{ + xml->get_widget("node_popup_gui_menuitem", _popup_gui_menuitem); + xml->get_widget("node_embed_gui_menuitem", _embed_gui_menuitem); + xml->get_widget("node_enabled_menuitem", _enabled_menuitem); + xml->get_widget("node_randomize_menuitem", _randomize_menuitem); +} + +void +NodeMenu::init(App& app, SPtr<const Client::BlockModel> block) +{ + ObjectMenu::init(app, block); + + _learn_menuitem->signal_activate().connect( + sigc::mem_fun(this, &NodeMenu::on_menu_learn)); + _popup_gui_menuitem->signal_activate().connect( + sigc::mem_fun(signal_popup_gui, &sigc::signal<void>::emit)); + _embed_gui_menuitem->signal_toggled().connect( + sigc::mem_fun(this, &NodeMenu::on_menu_embed_gui)); + _enabled_menuitem->signal_toggled().connect( + sigc::mem_fun(this, &NodeMenu::on_menu_enabled)); + _randomize_menuitem->signal_activate().connect( + sigc::mem_fun(this, &NodeMenu::on_menu_randomize)); + + SPtr<PluginModel> plugin = block->plugin_model(); + if (plugin) { + // Get the plugin to receive related presets + _preset_connection = plugin->signal_preset().connect( + sigc::mem_fun(this, &NodeMenu::add_preset)); + + if (!plugin->fetched()) { + _app->interface()->get(plugin->uri()); + plugin->set_fetched(true); + } + } + + if (plugin && plugin->has_ui()) { + _popup_gui_menuitem->show(); + _embed_gui_menuitem->show(); + const Atom& ui_embedded = block->get_property( + _app->uris().ingen_uiEmbedded); + _embed_gui_menuitem->set_active( + ui_embedded.is_valid() && ui_embedded.get<int32_t>()); + } else { + _popup_gui_menuitem->hide(); + _embed_gui_menuitem->hide(); + } + + const Atom& enabled = block->get_property(_app->uris().ingen_enabled); + _enabled_menuitem->set_active(!enabled.is_valid() || enabled.get<int32_t>()); + + if (plugin && _app->uris().lv2_Plugin == plugin->type()) { + _presets_menu = Gtk::manage(new Gtk::Menu()); + _presets_menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + "_Save Preset...", + sigc::mem_fun(this, &NodeMenu::on_save_preset_activated))); + _presets_menu->items().push_back(Gtk::Menu_Helpers::SeparatorElem()); + + for (const auto& p : plugin->presets()) { + add_preset(p.first, p.second); + } + + items().push_front( + Gtk::Menu_Helpers::ImageMenuElem( + "_Presets", + *(manage(new Gtk::Image(Gtk::Stock::INDEX, Gtk::ICON_SIZE_MENU))))); + + Gtk::MenuItem* presets_menu_item = &(items().front()); + presets_menu_item->set_submenu(*_presets_menu); + } + + if (has_control_inputs()) { + _randomize_menuitem->show(); + } else { + _randomize_menuitem->hide(); + } + + if (plugin && (plugin->uri() == "http://drobilla.net/ns/ingen-internals#Controller" + || plugin->uri() == "http://drobilla.net/ns/ingen-internals#Trigger")) { + _learn_menuitem->show(); + } else { + _learn_menuitem->hide(); + } + + if (!_popup_gui_menuitem->is_visible() && + !_embed_gui_menuitem->is_visible() && + !_randomize_menuitem->is_visible()) { + _separator_menuitem->hide(); + } + + _enable_signal = true; +} + +void +NodeMenu::add_preset(const URI& uri, const std::string& label) +{ + if (_presets_menu) { + _presets_menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + label, + sigc::bind(sigc::mem_fun(this, &NodeMenu::on_preset_activated), + uri))); + } +} + +void +NodeMenu::on_menu_embed_gui() +{ + signal_embed_gui.emit(_embed_gui_menuitem->get_active()); +} + +void +NodeMenu::on_menu_enabled() +{ + _app->set_property(_object->uri(), + _app->uris().ingen_enabled, + _app->forge().make(bool(_enabled_menuitem->get_active()))); +} + +void +NodeMenu::on_menu_randomize() +{ + _app->interface()->bundle_begin(); + + const SPtr<const BlockModel> bm = block(); + for (const auto& p : bm->ports()) { + if (p->is_input() && _app->can_control(p.get())) { + float min = 0.0f, max = 1.0f; + bm->port_value_range(p, min, max, _app->sample_rate()); + const float val = g_random_double_range(0.0, 1.0) * (max - min) + min; + _app->set_property(p->uri(), + _app->uris().ingen_value, + _app->forge().make(val)); + } + } + + _app->interface()->bundle_end(); +} + +void +NodeMenu::on_menu_disconnect() +{ + _app->interface()->disconnect_all(_object->parent()->path(), _object->path()); +} + +void +NodeMenu::on_save_preset_activated() +{ + Gtk::FileChooserDialog dialog("Save Preset", Gtk::FILE_CHOOSER_ACTION_SAVE); + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + dialog.set_default_response(Gtk::RESPONSE_OK); + dialog.set_current_folder(Glib::build_filename(Glib::get_home_dir(), ".lv2")); + + Gtk::HBox* extra = Gtk::manage(new Gtk::HBox()); + Gtk::Label* label = Gtk::manage(new Gtk::Label("URI (Optional): ")); + Gtk::Entry* entry = Gtk::manage(new Gtk::Entry()); + extra->pack_start(*label, false, true, 4); + extra->pack_start(*entry, true, true, 4); + extra->show_all(); + dialog.set_extra_widget(*Gtk::manage(extra)); + + if (dialog.run() == Gtk::RESPONSE_OK) { + const std::string user_uri = dialog.get_uri(); + const std::string user_path = Glib::filename_from_uri(user_uri); + const std::string dirname = Glib::path_get_dirname(user_path); + const std::string basename = Glib::path_get_basename(user_path); + const std::string sym = Raul::Symbol::symbolify(basename); + const std::string plugname = block()->plugin_model()->human_name(); + const std::string prefix = Raul::Symbol::symbolify(plugname); + const std::string bundle = prefix + "_" + sym + ".preset.lv2/"; + const std::string file = sym + ".ttl"; + const std::string real_path = Glib::build_filename(dirname, bundle, file); + const std::string real_uri = Glib::filename_to_uri(real_path); + + Properties props{ + { _app->uris().rdf_type, + _app->uris().pset_Preset }, + { _app->uris().rdfs_label, + _app->forge().alloc(basename) }, + { _app->uris().lv2_prototype, + _app->forge().make_urid(block()->uri()) }}; + _app->interface()->put(URI(real_uri), props); + } +} + +void +NodeMenu::on_preset_activated(const std::string& uri) +{ + _app->set_property(block()->uri(), + _app->uris().pset_preset, + _app->forge().make_urid(URI(uri))); +} + +bool +NodeMenu::has_control_inputs() +{ + for (const auto& p : block()->ports()) { + if (p->is_input() && p->is_numeric()) { + return true; + } + } + + return false; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/NodeMenu.hpp b/src/gui/NodeMenu.hpp new file mode 100644 index 00000000..5d9f1e6d --- /dev/null +++ b/src/gui/NodeMenu.hpp @@ -0,0 +1,75 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_NODEMENU_HPP +#define INGEN_GUI_NODEMENU_HPP + +#include <string> + +#include <gtkmm/builder.h> +#include <gtkmm/menu.h> +#include <gtkmm/menushell.h> + +#include "ObjectMenu.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/types.hpp" + +namespace Ingen { +namespace GUI { + +/** Menu for a Node. + * + * \ingroup GUI + */ +class NodeMenu : public ObjectMenu +{ +public: + NodeMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app, SPtr<const Client::BlockModel> block); + + bool has_control_inputs(); + + sigc::signal<void> signal_popup_gui; + sigc::signal<void, bool> signal_embed_gui; + +protected: + SPtr<const Client::BlockModel> block() const { + return dynamic_ptr_cast<const Client::BlockModel>(_object); + } + + void add_preset(const URI& uri, const std::string& label); + + void on_menu_disconnect(); + void on_menu_embed_gui(); + void on_menu_enabled(); + void on_menu_randomize(); + void on_save_preset_activated(); + void on_preset_activated(const std::string& uri); + + Gtk::MenuItem* _popup_gui_menuitem; + Gtk::CheckMenuItem* _embed_gui_menuitem; + Gtk::CheckMenuItem* _enabled_menuitem; + Gtk::MenuItem* _randomize_menuitem; + Gtk::Menu* _presets_menu; + sigc::connection _preset_connection; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_NODEMENU_HPP diff --git a/src/gui/NodeModule.cpp b/src/gui/NodeModule.cpp new file mode 100644 index 00000000..dadffff0 --- /dev/null +++ b/src/gui/NodeModule.cpp @@ -0,0 +1,518 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> + +#include <gtkmm/eventbox.h> + +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "ingen/Atom.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/client/PluginUI.hpp" + +#include "App.hpp" +#include "GraphCanvas.hpp" +#include "GraphWindow.hpp" +#include "NodeMenu.hpp" +#include "NodeModule.hpp" +#include "Port.hpp" +#include "RenameWindow.hpp" +#include "Style.hpp" +#include "SubgraphModule.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" +#include "ingen_config.h" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +NodeModule::NodeModule(GraphCanvas& canvas, + SPtr<const BlockModel> block) + : Ganv::Module(canvas, block->path().symbol(), 0, 0, true) + , _block(block) + , _gui_widget(nullptr) + , _gui_window(nullptr) + , _initialised(false) +{ + block->signal_new_port().connect( + sigc::mem_fun(this, &NodeModule::new_port_view)); + block->signal_removed_port().connect( + sigc::hide_return(sigc::mem_fun(this, &NodeModule::delete_port_view))); + block->signal_property().connect( + sigc::mem_fun(this, &NodeModule::property_changed)); + block->signal_moved().connect( + sigc::mem_fun(this, &NodeModule::rename)); + + signal_event().connect( + sigc::mem_fun(this, &NodeModule::on_event)); + + signal_moved().connect( + sigc::mem_fun(this, &NodeModule::store_location)); + + signal_selected().connect( + sigc::mem_fun(this, &NodeModule::on_selected)); + + const PluginModel* plugin = dynamic_cast<const PluginModel*>(block->plugin()); + if (plugin) { + plugin->signal_changed().connect( + sigc::mem_fun(this, &NodeModule::plugin_changed)); + } + + for (const auto& p : block->properties()) { + property_changed(p.first, p.second); + } + + if (_block->has_property(app().uris().ingen_uiEmbedded, + app().uris().forge.make(true))) { + // Schedule idle callback to embed GUI once ports arrive + Glib::signal_timeout().connect( + sigc::mem_fun(*this, &NodeModule::idle_init), 25, G_PRIORITY_DEFAULT_IDLE); + } else { + _initialised = true; + } +} + +NodeModule::~NodeModule() +{ + delete _gui_widget; + delete _gui_window; +} + +bool +NodeModule::idle_init() +{ + if (_block->ports().size() == 0) { + return true; // Need to embed GUI, but ports haven't shown up yet + } + + // Ports have arrived, embed GUI and deregister this callback + embed_gui(true); + _initialised = true; + return false; +} + +bool +NodeModule::show_menu(GdkEventButton* ev) +{ + WidgetFactory::get_widget_derived("object_menu", _menu); + if (!_menu) { + app().log().error("Failed to load object menu widget\n"); + return false; + } + + _menu->init(app(), _block); + _menu->signal_embed_gui.connect( + sigc::mem_fun(this, &NodeModule::on_embed_gui_toggled)); + _menu->signal_popup_gui.connect( + sigc::hide_return(sigc::mem_fun(this, &NodeModule::popup_gui))); + _menu->popup(ev->button, ev->time); + return true; +} + +NodeModule* +NodeModule::create(GraphCanvas& canvas, + SPtr<const BlockModel> block, + bool human) +{ + SPtr<const GraphModel> graph = dynamic_ptr_cast<const GraphModel>(block); + + NodeModule* ret = (graph) + ? new SubgraphModule(canvas, graph) + : new NodeModule(canvas, block); + + for (const auto& p : block->properties()) { + ret->property_changed(p.first, p.second); + } + + for (const auto& p : block->ports()) { + ret->new_port_view(p); + } + + ret->set_stacked(block->polyphonic()); + + if (human) { + ret->show_human_names(human); // FIXME: double port iteration + } + + return ret; +} + +App& +NodeModule::app() const +{ + return ((GraphCanvas*)canvas())->app(); +} + +void +NodeModule::show_human_names(bool b) +{ + const URIs& uris = app().uris(); + + if (b) { + set_label(block()->label().c_str()); + } else { + set_label(block()->symbol().c_str()); + } + + for (iterator i = begin(); i != end(); ++i) { + Ingen::GUI::Port* const port = dynamic_cast<Ingen::GUI::Port*>(*i); + Glib::ustring label(port->model()->symbol().c_str()); + if (b) { + const Atom& name_property = port->model()->get_property(uris.lv2_name); + if (name_property.type() == uris.forge.String) { + label = name_property.ptr<char>(); + } else { + Glib::ustring hn = block()->plugin_model()->port_human_name( + port->model()->index()); + if (!hn.empty()) { + label = hn; + } + } + } + (*i)->set_label(label.c_str()); + } +} + +void +NodeModule::port_activity(uint32_t index, const Atom& value) +{ + const URIs& uris = app().uris(); + if (!_plugin_ui) { + return; + } + + if (_block->get_port(index)->is_a(uris.atom_AtomPort)) { + _plugin_ui->port_event(index, + lv2_atom_total_size(value.atom()), + uris.atom_eventTransfer, + value.atom()); + } +} + +void +NodeModule::port_value_changed(uint32_t index, const Atom& value) +{ + const URIs& uris = app().uris(); + if (!_plugin_ui) { + return; + } + + if (value.type() == uris.atom_Float && + _block->get_port(index)->is_numeric()) { + _plugin_ui->port_event(index, sizeof(float), 0, value.ptr<float>()); + } else { + _plugin_ui->port_event(index, + lv2_atom_total_size(value.atom()), + uris.atom_eventTransfer, + value.atom()); + } +} + +void +NodeModule::plugin_changed() +{ + for (iterator p = begin(); p != end(); ++p) { + dynamic_cast<Ingen::GUI::Port*>(*p)->update_metadata(); + } +} + +void +NodeModule::on_embed_gui_toggled(bool embed) +{ + embed_gui(embed); + app().set_property(_block->uri(), + app().uris().ingen_uiEmbedded, + app().forge().make(embed)); +} + +void +NodeModule::embed_gui(bool embed) +{ + if (embed) { + if (_gui_window) { + app().log().warn("LV2 GUI already popped up, cannot embed\n"); + return; + } + + if (!_plugin_ui) { + _plugin_ui = _block->plugin_model()->ui(app().world(), _block); + } + + if (_plugin_ui) { + _plugin_ui->signal_property_changed().connect( + sigc::mem_fun(app(), &App::set_property)); + + if (!_plugin_ui->instantiate()) { + app().log().error("Failed to instantiate LV2 UI\n"); + } else { + GtkWidget* c_widget = (GtkWidget*)_plugin_ui->get_widget(); + _gui_widget = Glib::wrap(c_widget); + + Gtk::Container* container = new Gtk::EventBox(); + container->set_name("IngenEmbeddedUI"); + container->set_border_width(4.0); + container->add(*_gui_widget); + Ganv::Module::embed(container); + } + } else { + app().log().error("Failed to create LV2 UI\n"); + } + + if (_gui_widget) { + _gui_widget->show_all(); + set_control_values(); + } + + } else { // un-embed + Ganv::Module::embed(nullptr); + _plugin_ui.reset(); + } +} + +void +NodeModule::rename() +{ + if (app().world()->conf().option("port-labels").get<int32_t>() && + !app().world()->conf().option("human-names").get<int32_t>()) { + set_label(_block->path().symbol()); + } +} + +void +NodeModule::new_port_view(SPtr<const PortModel> port) +{ + Port::create(app(), *this, port); + + port->signal_value_changed().connect( + sigc::bind<0>(sigc::mem_fun(this, &NodeModule::port_value_changed), + port->index())); + + port->signal_activity().connect( + sigc::bind<0>(sigc::mem_fun(this, &NodeModule::port_activity), + port->index())); +} + +Port* +NodeModule::port(SPtr<const PortModel> model) +{ + for (iterator p = begin(); p != end(); ++p) { + Port* const port = dynamic_cast<Port*>(*p); + if (port->model() == model) { + return port; + } + } + return nullptr; +} + +void +NodeModule::delete_port_view(SPtr<const PortModel> model) +{ + Port* p = port(model); + if (p) { + delete p; + } else { + app().log().warn(fmt("Failed to find port %1% on module %2%\n") + % model->path() % _block->path()); + } +} + +bool +NodeModule::popup_gui() +{ + if (_block->plugin() && app().uris().lv2_Plugin == _block->plugin_model()->type()) { + if (_plugin_ui) { + app().log().warn("LV2 GUI already embedded, cannot pop up\n"); + return false; + } + + const PluginModel* const plugin = dynamic_cast<const PluginModel*>(_block->plugin()); + assert(plugin); + + _plugin_ui = plugin->ui(app().world(), _block); + + if (_plugin_ui) { + _plugin_ui->signal_property_changed().connect( + sigc::mem_fun(app(), &App::set_property)); + + if (!_plugin_ui->instantiated() && !_plugin_ui->instantiate()) { + app().log().error("Failed to instantiate LV2 UI\n"); + return false; + } + + GtkWidget* c_widget = (GtkWidget*)_plugin_ui->get_widget(); + _gui_widget = Glib::wrap(c_widget); + + _gui_window = new Gtk::Window(); + if (!_plugin_ui->is_resizable()) { + _gui_window->set_resizable(false); + } + _gui_window->set_title(_block->path() + " UI - Ingen"); + _gui_window->set_role("plugin_ui"); + _gui_window->add(*_gui_widget); + _gui_widget->show_all(); + set_control_values(); + + _gui_window->signal_unmap().connect( + sigc::mem_fun(this, &NodeModule::on_gui_window_close)); + _gui_window->present(); + + return true; + } else { + app().log().warn(fmt("No LV2 GUI for %1%\n") % _block->path()); + } + } + + return false; +} + +void +NodeModule::on_gui_window_close() +{ + delete _gui_window; + _gui_window = nullptr; + _plugin_ui.reset(); + _gui_widget = nullptr; +} + +void +NodeModule::set_control_values() +{ + uint32_t index = 0; + for (const auto& p : _block->ports()) { + if (app().can_control(p.get()) && p->value().is_valid()) { + port_value_changed(index, p->value()); + } + ++index; + } +} + +bool +NodeModule::on_double_click(GdkEventButton* event) +{ + popup_gui(); + return true; +} + +bool +NodeModule::on_event(GdkEvent* ev) +{ + if (ev->type == GDK_BUTTON_PRESS && ev->button.button == 3) { + return show_menu(&ev->button); + } else if (ev->type == GDK_2BUTTON_PRESS) { + return on_double_click(&ev->button); + } else if (ev->type == GDK_ENTER_NOTIFY) { + GraphBox* const box = app().window_factory()->graph_box( + dynamic_ptr_cast<const GraphModel>(_block->parent())); + if (box) { + box->object_entered(_block.get()); + } + } else if (ev->type == GDK_LEAVE_NOTIFY) { + GraphBox* const box = app().window_factory()->graph_box( + dynamic_ptr_cast<const GraphModel>(_block->parent())); + if (box) { + box->object_left(_block.get()); + } + } + + return false; +} + +void +NodeModule::store_location(double ax, double ay) +{ + const URIs& uris = app().uris(); + + const Atom x(app().forge().make(static_cast<float>(ax))); + const Atom y(app().forge().make(static_cast<float>(ay))); + + if (x != _block->get_property(uris.ingen_canvasX) || + y != _block->get_property(uris.ingen_canvasY)) + { + app().interface()->put(_block->uri(), {{uris.ingen_canvasX, x}, + {uris.ingen_canvasY, y}}); + } +} + +void +NodeModule::property_changed(const URI& key, const Atom& value) +{ + const URIs& uris = app().uris(); + if (value.type() == uris.forge.Float) { + if (key == uris.ingen_canvasX) { + move_to(value.get<float>(), get_y()); + } else if (key == uris.ingen_canvasY) { + move_to(get_x(), value.get<float>()); + } + } else if (value.type() == uris.forge.Bool) { + if (key == uris.ingen_polyphonic) { + set_stacked(value.get<int32_t>()); + } else if (key == uris.ingen_uiEmbedded && _initialised) { + if (value.get<int32_t>() && !_gui_widget) { + embed_gui(true); + } else if (!value.get<int32_t>() && _gui_widget) { + embed_gui(false); + } + } else if (key == uris.ingen_enabled) { + if (value.get<int32_t>()) { + set_dash_length(0.0); + } else { + set_dash_length(5.0); + } + } + } else if (value.type() == uris.forge.String) { + if (key == uris.lv2_name + && app().world()->conf().option("human-names").get<int32_t>()) { + set_label(value.ptr<char>()); + } + } +} + +bool +NodeModule::on_selected(gboolean selected) +{ + GraphWindow* win = app().window_factory()->parent_graph_window(block()); + if (!win) { + return true; + } + + if (selected && win->documentation_is_visible()) { + GraphWindow* win = app().window_factory()->parent_graph_window(block()); + std::string doc; + bool html = false; +#ifdef HAVE_WEBKIT + html = true; +#endif + if (block()->plugin_model()) { + doc = block()->plugin_model()->documentation(html); + } + win->set_documentation(doc, html); + } + + return true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/NodeModule.hpp b/src/gui/NodeModule.hpp new file mode 100644 index 00000000..863b6ffb --- /dev/null +++ b/src/gui/NodeModule.hpp @@ -0,0 +1,104 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_NODEMODULE_HPP +#define INGEN_GUI_NODEMODULE_HPP + +#include "ganv/Module.hpp" +#include "ingen/types.hpp" + +#include "Port.hpp" + +namespace Raul { class Atom; } + +namespace Ingen { namespace Client { +class BlockModel; +class PluginUI; +class PortModel; +} } + +namespace Ingen { +namespace GUI { + +class GraphCanvas; +class Port; +class NodeMenu; + +/** A module in a graphn. + * + * This base class is extended for various types of modules. + * + * \ingroup GUI + */ +class NodeModule : public Ganv::Module +{ +public: + static NodeModule* create( + GraphCanvas& canvas, + SPtr<const Client::BlockModel> block, + bool human); + + virtual ~NodeModule(); + + App& app() const; + + Port* port(SPtr<const Client::PortModel> model); + + void delete_port_view(SPtr<const Client::PortModel> model); + + virtual void store_location(double ax, double ay); + void show_human_names(bool b); + + SPtr<const Client::BlockModel> block() const { return _block; } + +protected: + NodeModule(GraphCanvas& canvas, SPtr<const Client::BlockModel> block); + + virtual bool on_double_click(GdkEventButton* ev); + + bool idle_init(); + bool on_event(GdkEvent* ev); + + void on_embed_gui_toggled(bool embed); + void embed_gui(bool embed); + bool popup_gui(); + void on_gui_window_close(); + bool on_selected(gboolean selected); + + void rename(); + void property_changed(const URI& key, const Atom& value); + + void new_port_view(SPtr<const Client::PortModel> port); + + void port_activity(uint32_t index, const Atom& value); + void port_value_changed(uint32_t index, const Atom& value); + void plugin_changed(); + void set_control_values(); + + bool show_menu(GdkEventButton* ev); + + SPtr<const Client::BlockModel> _block; + NodeMenu* _menu; + SPtr<Client::PluginUI> _plugin_ui; + Gtk::Widget* _gui_widget; + Gtk::Window* _gui_window; ///< iff popped up + bool _initialised; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_NODEMODULE_HPP diff --git a/src/gui/ObjectMenu.cpp b/src/gui/ObjectMenu.cpp new file mode 100644 index 00000000..bfce4248 --- /dev/null +++ b/src/gui/ObjectMenu.cpp @@ -0,0 +1,145 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <utility> + +#include "ingen/Forge.hpp" +#include "ingen/Interface.hpp" +#include "ingen/client/ObjectModel.hpp" + +#include "App.hpp" +#include "ObjectMenu.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +ObjectMenu::ObjectMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::Menu(cobject) + , _app(nullptr) + , _polyphonic_menuitem(nullptr) + , _disconnect_menuitem(nullptr) + , _rename_menuitem(nullptr) + , _destroy_menuitem(nullptr) + , _properties_menuitem(nullptr) + , _enable_signal(false) +{ + xml->get_widget("object_learn_menuitem", _learn_menuitem); + xml->get_widget("object_unlearn_menuitem", _unlearn_menuitem); + xml->get_widget("object_polyphonic_menuitem", _polyphonic_menuitem); + xml->get_widget("object_disconnect_menuitem", _disconnect_menuitem); + xml->get_widget("object_rename_menuitem", _rename_menuitem); + xml->get_widget("object_destroy_menuitem", _destroy_menuitem); + xml->get_widget("object_properties_menuitem", _properties_menuitem); + xml->get_widget("object_menu_separator", _separator_menuitem); +} + +void +ObjectMenu::init(App& app, SPtr<const ObjectModel> object) +{ + _app = &app; + _object = object; + + _polyphonic_menuitem->signal_toggled().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_polyphonic)); + + _polyphonic_menuitem->set_active(object->polyphonic()); + + _learn_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_learn)); + + _unlearn_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_unlearn)); + + _disconnect_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_disconnect)); + + _rename_menuitem->signal_activate().connect( + sigc::bind(sigc::mem_fun(_app->window_factory(), &WindowFactory::present_rename), + object)); + + _destroy_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_destroy)); + + _properties_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_properties)); + + object->signal_property().connect(sigc::mem_fun(this, &ObjectMenu::property_changed)); + + _learn_menuitem->hide(); + _unlearn_menuitem->hide(); + + _enable_signal = true; +} + +void +ObjectMenu::on_menu_learn() +{ + _app->interface()->set_property(_object->uri(), + _app->uris().midi_binding, + _app->uris().patch_wildcard.urid); +} + +void +ObjectMenu::on_menu_unlearn() +{ + Properties remove; + remove.emplace(_app->uris().midi_binding, + Property(_app->uris().patch_wildcard)); + _app->interface()->delta(_object->uri(), remove, Properties()); +} + +void +ObjectMenu::on_menu_polyphonic() +{ + if (_enable_signal) { + _app->set_property( + _object->uri(), + _app->uris().ingen_polyphonic, + _app->forge().make(bool(_polyphonic_menuitem->get_active()))); + } +} + +void +ObjectMenu::property_changed(const URI& predicate, const Atom& value) +{ + const URIs& uris = _app->uris(); + _enable_signal = false; + if (predicate == uris.ingen_polyphonic && value.type() == uris.forge.Bool) { + _polyphonic_menuitem->set_active(value.get<int32_t>()); + } + _enable_signal = true; +} + +void +ObjectMenu::on_menu_destroy() +{ + _app->interface()->del(_object->uri()); +} + +void +ObjectMenu::on_menu_properties() +{ + _app->window_factory()->present_properties(_object); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/ObjectMenu.hpp b/src/gui/ObjectMenu.hpp new file mode 100644 index 00000000..a9b07fd5 --- /dev/null +++ b/src/gui/ObjectMenu.hpp @@ -0,0 +1,77 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_OBJECTMENU_HPP +#define INGEN_GUI_OBJECTMENU_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/checkmenuitem.h> +#include <gtkmm/menu.h> +#include <gtkmm/menuitem.h> + +#include "ingen/client/ObjectModel.hpp" +#include "ingen/types.hpp" + +namespace Ingen { +namespace GUI { + +class ObjectControlWindow; +class ObjectPropertiesWindow; +class GraphCanvas; + +/** Menu for a Object. + * + * \ingroup GUI + */ +class ObjectMenu : public Gtk::Menu +{ +public: + ObjectMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app, SPtr<const Client::ObjectModel> object); + + SPtr<const Client::ObjectModel> object() const { return _object; } + App* app() const { return _app; } + +protected: + void on_menu_learn(); + void on_menu_unlearn(); + virtual void on_menu_disconnect() = 0; + void on_menu_polyphonic(); + void on_menu_destroy(); + void on_menu_properties(); + + void property_changed(const URI& predicate, const Atom& value); + + App* _app; + SPtr<const Client::ObjectModel> _object; + Gtk::MenuItem* _learn_menuitem; + Gtk::MenuItem* _unlearn_menuitem; + Gtk::CheckMenuItem* _polyphonic_menuitem; + Gtk::MenuItem* _disconnect_menuitem; + Gtk::MenuItem* _rename_menuitem; + Gtk::MenuItem* _destroy_menuitem; + Gtk::MenuItem* _properties_menuitem; + Gtk::SeparatorMenuItem* _separator_menuitem; + + bool _enable_signal; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_OBJECTMENU_HPP diff --git a/src/gui/PluginMenu.cpp b/src/gui/PluginMenu.cpp new file mode 100644 index 00000000..fc385773 --- /dev/null +++ b/src/gui/PluginMenu.cpp @@ -0,0 +1,176 @@ +/* + This file is part of Ingen. + Copyright 2014-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "PluginMenu.hpp" +#include "ingen/Log.hpp" +#include "ingen/client/PluginModel.hpp" + +namespace Ingen { +namespace GUI { + +PluginMenu::PluginMenu(Ingen::World& world) + : _world(world) + , _classless_menu(nullptr, nullptr) +{ + clear(); +} + +void +PluginMenu::clear() +{ + const LilvWorld* lworld = _world.lilv_world(); + const LilvPluginClass* lv2_plugin = lilv_world_get_plugin_class(lworld); + const LilvPluginClasses* classes = lilv_world_get_plugin_classes(lworld); + + // Empty completely + _classless_menu = MenuRecord(nullptr, nullptr); + _class_menus.clear(); + items().clear(); + + // Build skeleton + LV2Children children; + LILV_FOREACH(plugin_classes, i, classes) { + const LilvPluginClass* c = lilv_plugin_classes_get(classes, i); + const LilvNode* p = lilv_plugin_class_get_parent_uri(c); + if (!p) { + p = lilv_plugin_class_get_uri(lv2_plugin); + } + children.emplace(lilv_node_as_string(p), c); + } + + std::set<const char*> ancestors; + build_plugin_class_menu(this, lv2_plugin, classes, children, ancestors); + + items().push_back(Gtk::Menu_Helpers::MenuElem("_Uncategorized")); + _classless_menu.item = &(items().back()); + _classless_menu.menu = Gtk::manage(new Gtk::Menu()); + _classless_menu.item->set_submenu(*_classless_menu.menu); + _classless_menu.item->hide(); +} + +void +PluginMenu::add_plugin(SPtr<Client::PluginModel> p) +{ + typedef ClassMenus::iterator iterator; + + if (!p->lilv_plugin() || lilv_plugin_is_replaced(p->lilv_plugin())) { + return; + } + + const LilvPluginClass* pc = lilv_plugin_get_class(p->lilv_plugin()); + const LilvNode* class_uri = lilv_plugin_class_get_uri(pc); + const char* class_uri_str = lilv_node_as_string(class_uri); + + std::pair<iterator, iterator> range = _class_menus.equal_range(class_uri_str); + if (range.first == _class_menus.end() || range.first == range.second + || range.first->second.menu == this) { + // Add to uncategorized plugin menu + add_plugin_to_menu(_classless_menu, p); + } else { + // For each menu that represents plugin's class (possibly several) + for (auto i = range.first; i != range.second ; ++i) { + add_plugin_to_menu(i->second, p); + } + } +} + +size_t +PluginMenu::build_plugin_class_menu(Gtk::Menu* menu, + const LilvPluginClass* plugin_class, + const LilvPluginClasses* classes, + const LV2Children& children, + std::set<const char*>& ancestors) +{ + size_t num_items = 0; + const LilvNode* class_uri = lilv_plugin_class_get_uri(plugin_class); + const char* class_uri_str = lilv_node_as_string(class_uri); + + const std::pair<LV2Children::const_iterator, LV2Children::const_iterator> kids + = children.equal_range(class_uri_str); + + if (kids.first == children.end()) { + return 0; + } + + // Add submenus + ancestors.insert(class_uri_str); + for (LV2Children::const_iterator i = kids.first; i != kids.second; ++i) { + const LilvPluginClass* c = i->second; + const char* sub_label_str = lilv_node_as_string(lilv_plugin_class_get_label(c)); + const char* sub_uri_str = lilv_node_as_string(lilv_plugin_class_get_uri(c)); + if (ancestors.find(sub_uri_str) != ancestors.end()) { + _world.log().warn(fmt("Infinite LV2 class recursion: %1% <: %2%\n") + % class_uri_str % sub_uri_str); + return 0; + } + + Gtk::Menu_Helpers::MenuElem menu_elem = Gtk::Menu_Helpers::MenuElem( + std::string("_") + sub_label_str); + menu->items().push_back(menu_elem); + Gtk::MenuItem* menu_item = &(menu->items().back()); + + Gtk::Menu* submenu = Gtk::manage(new Gtk::Menu()); + menu_item->set_submenu(*submenu); + + size_t num_child_items = build_plugin_class_menu( + submenu, c, classes, children, ancestors); + + _class_menus.emplace(sub_uri_str, MenuRecord(menu_item, submenu)); + if (num_child_items == 0) { + menu_item->hide(); + } + + ++num_items; + } + ancestors.erase(class_uri_str); + + return num_items; +} + +void +PluginMenu::add_plugin_to_menu(MenuRecord& menu, SPtr<Client::PluginModel> p) +{ + const URIs& uris = _world.uris(); + LilvWorld* lworld = _world.lilv_world(); + LilvNode* ingen_Graph = lilv_new_uri(lworld, uris.ingen_Graph.c_str()); + LilvNode* rdf_type = lilv_new_uri(lworld, uris.rdf_type.c_str()); + + bool is_graph = lilv_world_ask(lworld, + lilv_plugin_get_uri(p->lilv_plugin()), + rdf_type, + ingen_Graph); + + menu.menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + std::string("_") + p->human_name() + (is_graph ? " ⚙" : ""), + sigc::bind(sigc::mem_fun(this, &PluginMenu::load_plugin), p))); + + if (!menu.item->is_visible()) { + menu.item->show(); + } + + lilv_node_free(rdf_type); + lilv_node_free(ingen_Graph); +} + +void +PluginMenu::load_plugin(WPtr<Client::PluginModel> weak_plugin) +{ + signal_load_plugin.emit(weak_plugin); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/PluginMenu.hpp b/src/gui/PluginMenu.hpp new file mode 100644 index 00000000..bc654db5 --- /dev/null +++ b/src/gui/PluginMenu.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2014-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_PLUGINMENU_HPP +#define INGEN_GUI_PLUGINMENU_HPP + +#include <map> +#include <set> +#include <string> + +#include <gtkmm/menu.h> + +#include "ingen/World.hpp" +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +namespace Ingen { + +namespace Client { class PluginModel; } + +namespace GUI { + +/** + Type-hierarchical plugin menu. + + @ingroup GUI +*/ +class PluginMenu : public Gtk::Menu +{ +public: + PluginMenu(Ingen::World& world); + + void clear(); + void add_plugin(SPtr<Client::PluginModel> p); + + sigc::signal< void, WPtr<Client::PluginModel> > signal_load_plugin; + +private: + struct MenuRecord { + MenuRecord(Gtk::MenuItem* i, Gtk::Menu* m) : item(i), menu(m) {} + Gtk::MenuItem* item; + Gtk::Menu* menu; + }; + + typedef std::multimap<const std::string, const LilvPluginClass*> LV2Children; + typedef std::multimap<const std::string, MenuRecord> ClassMenus; + + /// Recursively add hierarchy rooted at `plugin_class` to `menu`. + size_t build_plugin_class_menu(Gtk::Menu* menu, + const LilvPluginClass* plugin_class, + const LilvPluginClasses* classes, + const LV2Children& children, + std::set<const char*>& ancestors); + + void add_plugin_to_menu(MenuRecord& menu, SPtr<Client::PluginModel> p); + + void load_plugin(WPtr<Client::PluginModel> weak_plugin); + + Ingen::World& _world; + MenuRecord _classless_menu; + ClassMenus _class_menus; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_PLUGINMENU_HPP diff --git a/src/gui/Port.cpp b/src/gui/Port.cpp new file mode 100644 index 00000000..9742cee3 --- /dev/null +++ b/src/gui/Port.cpp @@ -0,0 +1,534 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> + +#include "ganv/Module.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PortModel.hpp" + +#include "App.hpp" +#include "GraphWindow.hpp" +#include "Port.hpp" +#include "PortMenu.hpp" +#include "RDFS.hpp" +#include "Style.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" +#include "ingen_config.h" +#include "rgba.hpp" + +using namespace Ingen::Client; + +namespace Ingen { +namespace GUI { + +Port* +Port::create(App& app, + Ganv::Module& module, + SPtr<const PortModel> pm, + bool flip) +{ + return new Port(app, module, pm, port_label(app, pm), flip); +} + +/** @param flip Make an input port appear as an output port, and vice versa. + */ +Port::Port(App& app, + Ganv::Module& module, + SPtr<const PortModel> pm, + const std::string& name, + bool flip) + : Ganv::Port(module, name, + flip ? (!pm->is_input()) : pm->is_input(), + app.style()->get_port_color(pm.get())) + , _app(app) + , _port_model(pm) + , _entered(false) + , _flipped(flip) +{ + assert(pm); + + if (app.can_control(pm.get())) { + show_control(); + pm->signal_value_changed().connect( + sigc::mem_fun(this, &Port::value_changed)); + } + + port_properties_changed(); + + pm->signal_property().connect( + sigc::mem_fun(this, &Port::property_changed)); + pm->signal_property_removed().connect( + sigc::mem_fun(this, &Port::property_removed)); + pm->signal_activity().connect( + sigc::mem_fun(this, &Port::activity)); + pm->signal_moved().connect( + sigc::mem_fun(this, &Port::moved)); + + signal_value_changed.connect( + sigc::mem_fun(this, &Port::on_value_changed)); + + signal_event().connect( + sigc::mem_fun(this, &Port::on_event)); + + set_is_controllable(pm->is_numeric() && pm->is_input()); + + Ganv::Port::set_beveled(model()->is_a(_app.uris().lv2_ControlPort) || + model()->has_property(_app.uris().atom_bufferType, + _app.uris().atom_Sequence)); + + for (const auto& p : pm->properties()) { + property_changed(p.first, p.second); + } + + update_metadata(); + value_changed(pm->value()); +} + +Port::~Port() +{ + _app.activity_port_destroyed(this); +} + +std::string +Port::port_label(App& app, SPtr<const PortModel> pm) +{ + if (!pm) { + return ""; + } + + std::string label; + if (app.world()->conf().option("port-labels").get<int32_t>()) { + if (app.world()->conf().option("human-names").get<int32_t>()) { + const Atom& name = pm->get_property(app.uris().lv2_name); + if (name.type() == app.forge().String) { + label = name.ptr<char>(); + } else { + const SPtr<const BlockModel> parent( + dynamic_ptr_cast<const BlockModel>(pm->parent())); + if (parent && parent->plugin_model()) { + label = parent->plugin_model()->port_human_name(pm->index()); + } + } + } else { + label = pm->path().symbol(); + } + } + return label; +} + +void +Port::ensure_label() +{ + if (!get_label()) { + set_label(port_label(_app, _port_model.lock()).c_str()); + } +} + +void +Port::update_metadata() +{ + SPtr<const PortModel> pm = _port_model.lock(); + if (pm && _app.can_control(pm.get()) && pm->is_numeric()) { + SPtr<const BlockModel> parent = dynamic_ptr_cast<const BlockModel>(pm->parent()); + if (parent) { + float min = 0.0f; + float max = 1.0f; + parent->port_value_range(pm, min, max, _app.sample_rate()); + set_control_min(min); + set_control_max(max); + } + } +} + +bool +Port::show_menu(GdkEventButton* ev) +{ + PortMenu* menu = nullptr; + WidgetFactory::get_widget_derived("object_menu", menu); + if (!menu) { + _app.log().error("Failed to load port menu widget\n"); + return false; + } + + menu->init(_app, model(), _flipped); + menu->popup(ev->button, ev->time); + return true; +} + +void +Port::moved() +{ + if (_app.world()->conf().option("port-labels").get<int32_t>() && + !_app.world()->conf().option("human-names").get<int32_t>()) { + set_label(model()->symbol().c_str()); + } +} + +void +Port::on_value_changed(double value) +{ + const URIs& uris = _app.uris(); + const Atom& current_value = model()->value(); + if (current_value.type() != uris.forge.Float) { + return; // Non-float, unsupported + } + + if (current_value.get<float>() == (float)value) { + return; // No change + } + + const Atom atom = _app.forge().make(float(value)); + _app.set_property(model()->uri(), + _app.world()->uris().ingen_value, + atom); + + if (_entered) { + GraphBox* box = get_graph_box(); + if (box) { + box->show_port_status(model().get(), atom); + } + } +} + +void +Port::value_changed(const Atom& value) +{ + if (value.type() == _app.forge().Float && !get_grabbed()) { + Ganv::Port::set_control_value(value.get<float>()); + } +} + +void +Port::on_scale_point_activated(float f) +{ + _app.set_property(model()->uri(), + _app.world()->uris().ingen_value, + _app.world()->forge().make(f)); +} + +Gtk::Menu* +Port::build_enum_menu() +{ + SPtr<const BlockModel> block = dynamic_ptr_cast<BlockModel>(model()->parent()); + Gtk::Menu* menu = Gtk::manage(new Gtk::Menu()); + + PluginModel::ScalePoints points = block->plugin_model()->port_scale_points( + model()->index()); + for (auto i = points.begin(); i != points.end(); ++i) { + menu->items().push_back(Gtk::Menu_Helpers::MenuElem(i->second)); + Gtk::MenuItem* menu_item = &(menu->items().back()); + menu_item->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &Port::on_scale_point_activated), + i->first)); + } + + return menu; +} + +void +Port::on_uri_activated(const URI& uri) +{ + _app.set_property(model()->uri(), + _app.world()->uris().ingen_value, + _app.world()->forge().make_urid( + _app.world()->uri_map().map_uri(uri.c_str()))); +} + +Gtk::Menu* +Port::build_uri_menu() +{ + World* world = _app.world(); + SPtr<const BlockModel> block = dynamic_ptr_cast<BlockModel>(model()->parent()); + Gtk::Menu* menu = Gtk::manage(new Gtk::Menu()); + + // Get the port designation, which should be a rdf:Property + const Atom& designation_atom = model()->get_property( + _app.uris().lv2_designation); + if (!designation_atom.is_valid()) { + return nullptr; + } + + LilvNode* designation = lilv_new_uri( + world->lilv_world(), world->forge().str(designation_atom, false).c_str()); + LilvNode* rdfs_range = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "range"); + + // Get every class in the range of the port's property + RDFS::URISet ranges; + LilvNodes* range = lilv_world_find_nodes( + world->lilv_world(), designation, rdfs_range, nullptr); + LILV_FOREACH(nodes, r, range) { + ranges.insert(URI(lilv_node_as_string(lilv_nodes_get(range, r)))); + } + RDFS::classes(world, ranges, false); + + // Get all objects in range + RDFS::Objects values = RDFS::instances(world, ranges); + + // Add a menu item for each such class + for (const auto& v : values) { + if (!v.first.empty()) { + const std::string qname = world->rdf_world()->prefixes().qualify(v.second); + const std::string label = qname + " - " + v.first; + menu->items().push_back(Gtk::Menu_Helpers::MenuElem(label)); + Gtk::MenuItem* menu_item = &(menu->items().back()); + menu_item->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &Port::on_uri_activated), + v.second)); + } + } + + return menu; +} + +bool +Port::on_event(GdkEvent* ev) +{ + GraphBox* box = nullptr; + switch (ev->type) { + case GDK_ENTER_NOTIFY: + _entered = true; + if ((box = get_graph_box())) { + box->object_entered(model().get()); + } + return false; + case GDK_LEAVE_NOTIFY: + _entered = false; + if ((box = get_graph_box())) { + box->object_left(model().get()); + } + return false; + case GDK_BUTTON_PRESS: + if (ev->button.button == 1) { + if (model()->is_enumeration()) { + Gtk::Menu* menu = build_enum_menu(); + menu->popup(ev->button.button, ev->button.time); + return true; + } else if (model()->is_uri()) { + Gtk::Menu* menu = build_uri_menu(); + if (menu) { + menu->popup(ev->button.button, ev->button.time); + return true; + } + } + } else if (ev->button.button == 3) { + return show_menu(&ev->button); + } + break; + default: + break; + } + + return false; +} + +inline static uint32_t +peak_color(float peak) +{ + static const uint32_t min = 0x4A8A0EC0; + static const uint32_t max = 0xFFCE1FC0; + static const uint32_t peak_min = 0xFF561FC0; + static const uint32_t peak_max = 0xFF0A38C0; + + if (peak < 1.0) { + return rgba_interpolate(min, max, peak); + } else { + return rgba_interpolate(peak_min, peak_max, fminf(peak, 2.0f) - 1.0f); + } +} + +void +Port::activity(const Atom& value) +{ + if (model()->is_a(_app.uris().lv2_AudioPort)) { + set_fill_color(peak_color(value.get<float>())); + } else if (_app.can_control(model().get()) && value.type() == _app.uris().atom_Float) { + Ganv::Port::set_control_value(value.get<float>()); + } else { + _app.port_activity(this); + } +} + +GraphBox* +Port::get_graph_box() const +{ + SPtr<const GraphModel> graph = dynamic_ptr_cast<const GraphModel>(model()->parent()); + GraphBox* box = _app.window_factory()->graph_box(graph); + if (!box) { + graph = dynamic_ptr_cast<const GraphModel>(model()->parent()->parent()); + box = _app.window_factory()->graph_box(graph); + } + return box; +} + +void +Port::set_type_tag() +{ + const URIs& uris = _app.uris(); + std::string tag; + if (model()->is_a(_app.uris().lv2_AudioPort)) { + tag = "~"; + } else if (model()->is_a(_app.uris().lv2_CVPort)) { + tag = "ℝ̰"; + } else if (model()->is_a(_app.uris().lv2_ControlPort)) { + if (model()->is_enumeration()) { + tag = "…"; + } else if (model()->is_integer()) { + tag = "ℤ"; + } else if (model()->is_toggle()) { + tag = ((model()->value() != _app.uris().forge.make(0.0f)) + ? "☑" : "☐"); + + } else { + tag = "ℝ"; + } + } else if (model()->is_a(_app.uris().atom_AtomPort)) { + if (model()->supports(_app.uris().atom_Float)) { + if (model()->is_toggle()) { + tag = ((model()->value() != _app.uris().forge.make(0.0f)) + ? "☑" : "☐"); + } else { + tag = "ℝ"; + } + } + if (model()->supports(_app.uris().atom_Int)) { + tag += "ℤ"; + } + if (model()->supports(_app.uris().midi_MidiEvent)) { + tag += "𝕄"; + } + if (model()->supports(_app.uris().patch_Message)) { + if (tag.empty()) { + tag += "="; + } else { + tag += "̿"; + } + } + if (tag.empty()) { + tag = "*"; + } + + if (model()->has_property(uris.atom_bufferType, uris.atom_Sequence)) { + tag += "̤"; + } + } + + if (!tag.empty()) { + set_value_label(tag.c_str()); + } +} + +void +Port::port_properties_changed() +{ + if (model()->is_toggle()) { + set_control_is_toggle(true); + } else if (model()->is_integer()) { + set_control_is_integer(true); + } + set_type_tag(); +} + +void +Port::property_changed(const URI& key, const Atom& value) +{ + const URIs& uris = _app.uris(); + if (value.type() == uris.forge.Float) { + float val = value.get<float>(); + if (key == uris.ingen_value && !get_grabbed()) { + Ganv::Port::set_control_value(val); + if (model()->is_toggle()) { + std::string tag = (val == 0.0f) ? "☐" : "☑"; + if (model()->is_a(_app.uris().lv2_CVPort)) { + tag += "̰"; + } else if (model()->has_property(uris.atom_bufferType, + uris.atom_Sequence)) { + tag += "̤"; + } + set_value_label(tag.c_str()); + } + } else if (key == uris.lv2_minimum) { + if (model()->port_property(uris.lv2_sampleRate)) { + val *= _app.sample_rate(); + } + set_control_min(val); + } else if (key == uris.lv2_maximum) { + if (model()->port_property(uris.lv2_sampleRate)) { + val *= _app.sample_rate(); + } + set_control_max(val); + } + } else if (key == uris.lv2_portProperty) { + port_properties_changed(); + } else if (key == uris.lv2_name) { + if (value.type() == uris.forge.String && + _app.world()->conf().option("port-labels").get<int32_t>() && + _app.world()->conf().option("human-names").get<int32_t>()) { + set_label(value.ptr<char>()); + } + } else if (key == uris.rdf_type || key == uris.atom_bufferType) { + set_fill_color(_app.style()->get_port_color(model().get())); + Ganv::Port::set_beveled(model()->is_a(uris.lv2_ControlPort) || + model()->has_property(uris.atom_bufferType, + uris.atom_Sequence)); + } +} + +void +Port::property_removed(const URI& key, const Atom& value) +{ + const URIs& uris = _app.uris(); + if (key == uris.lv2_minimum || key == uris.lv2_maximum) { + update_metadata(); + } else if (key == uris.rdf_type || key == uris.atom_bufferType) { + Ganv::Port::set_beveled(model()->is_a(uris.lv2_ControlPort) || + model()->has_property(uris.atom_bufferType, + uris.atom_Sequence)); + } +} + +bool +Port::on_selected(gboolean b) +{ + if (b) { + SPtr<const PortModel> pm = _port_model.lock(); + if (pm) { + SPtr<const BlockModel> block = dynamic_ptr_cast<const BlockModel>(pm->parent()); + GraphWindow* win = _app.window_factory()->parent_graph_window(block); + if (win && win->documentation_is_visible() && block->plugin_model()) { + bool html = false; +#ifdef HAVE_WEBKIT + html = true; +#endif + const std::string& doc = block->plugin_model()->port_documentation( + pm->index(), html); + win->set_documentation(doc, html); + } + } + } + + return true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/Port.hpp b/src/gui/Port.hpp new file mode 100644 index 00000000..c714feae --- /dev/null +++ b/src/gui/Port.hpp @@ -0,0 +1,102 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_PORT_HPP +#define INGEN_GUI_PORT_HPP + +#include <cassert> +#include <string> + +#include <gtkmm/menu.h> + +#include "ganv/Port.hpp" +#include "ingen/types.hpp" + +namespace Raul { +class Atom; +} + +namespace Ingen { + +class URI; + +namespace Client { class PortModel; } + +namespace GUI { + +class App; +class GraphBox; + +/** A Port on an Module. + * + * \ingroup GUI + */ +class Port : public Ganv::Port +{ +public: + static Port* create( + App& app, + Ganv::Module& module, + SPtr<const Client::PortModel> pm, + bool flip = false); + + ~Port(); + + SPtr<const Client::PortModel> model() const { return _port_model.lock(); } + + bool show_menu(GdkEventButton* ev); + void update_metadata(); + void ensure_label(); + + void value_changed(const Atom& value); + void activity(const Atom& value); + + bool on_selected(gboolean b); + +private: + Port(App& app, + Ganv::Module& module, + SPtr<const Client::PortModel> pm, + const std::string& name, + bool flip = false); + + static std::string port_label(App& app, SPtr<const Client::PortModel> pm); + + Gtk::Menu* build_enum_menu(); + Gtk::Menu* build_uri_menu(); + GraphBox* get_graph_box() const; + + void property_changed(const URI& key, const Atom& value); + void property_removed(const URI& key, const Atom& value); + void moved(); + + void on_value_changed(double value); + void on_scale_point_activated(float f); + void on_uri_activated(const URI& uri); + bool on_event(GdkEvent* ev); + void port_properties_changed(); + void set_type_tag(); + + App& _app; + WPtr<const Client::PortModel> _port_model; + bool _entered : 1; + bool _flipped : 1; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_PORT_HPP diff --git a/src/gui/PortMenu.cpp b/src/gui/PortMenu.cpp new file mode 100644 index 00000000..c6ec8fa1 --- /dev/null +++ b/src/gui/PortMenu.cpp @@ -0,0 +1,174 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/Interface.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PortModel.hpp" +#include "ingen/types.hpp" + +#include "App.hpp" +#include "PortMenu.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +PortMenu::PortMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : ObjectMenu(cobject, xml) + , _internal_graph_port(false) +{ + xml->get_widget("object_menu", _port_menu); + xml->get_widget("port_set_min_menuitem", _set_min_menuitem); + xml->get_widget("port_set_max_menuitem", _set_max_menuitem); + xml->get_widget("port_reset_range_menuitem", _reset_range_menuitem); + xml->get_widget("port_expose_menuitem", _expose_menuitem); +} + +void +PortMenu::init(App& app, SPtr<const PortModel> port, bool internal_graph_port) +{ + const URIs& uris = app.uris(); + + ObjectMenu::init(app, port); + _internal_graph_port = internal_graph_port; + + _set_min_menuitem->signal_activate().connect( + sigc::mem_fun(this, &PortMenu::on_menu_set_min)); + + _set_max_menuitem->signal_activate().connect( + sigc::mem_fun(this, &PortMenu::on_menu_set_max)); + + _reset_range_menuitem->signal_activate().connect( + sigc::mem_fun(this, &PortMenu::on_menu_reset_range)); + + _expose_menuitem->signal_activate().connect( + sigc::mem_fun(this, &PortMenu::on_menu_expose)); + + const bool is_control(app.can_control(port.get()) && port->is_numeric()); + const bool is_on_graph(dynamic_ptr_cast<GraphModel>(port->parent())); + const bool is_input(port->is_input()); + + if (!is_on_graph) { + _polyphonic_menuitem->set_sensitive(false); + _rename_menuitem->set_sensitive(false); + _destroy_menuitem->set_sensitive(false); + } + + if (port->is_a(uris.atom_AtomPort)) { + _polyphonic_menuitem->hide(); + } + + _reset_range_menuitem->set_visible(is_input && is_control && !is_on_graph); + _set_max_menuitem->set_visible(is_input && is_control); + _set_min_menuitem->set_visible(is_input && is_control); + _expose_menuitem->set_visible(!is_on_graph); + _learn_menuitem->set_visible(is_input && is_control); + _unlearn_menuitem->set_visible(is_input && is_control); + + if (!is_control && is_on_graph) { + _separator_menuitem->hide(); + } + + _enable_signal = true; +} + +void +PortMenu::on_menu_disconnect() +{ + if (_internal_graph_port) { + _app->interface()->disconnect_all( + _object->parent()->path(), _object->path()); + } else { + _app->interface()->disconnect_all( + _object->parent()->path().parent(), _object->path()); + } +} + +void +PortMenu::on_menu_set_min() +{ + const URIs& uris = _app->uris(); + SPtr<const PortModel> model = dynamic_ptr_cast<const PortModel>(_object); + const Atom& value = model->get_property(uris.ingen_value); + if (value.is_valid()) { + _app->set_property(_object->uri(), uris.lv2_minimum, value); + } +} + +void +PortMenu::on_menu_set_max() +{ + const URIs& uris = _app->uris(); + SPtr<const PortModel> model = dynamic_ptr_cast<const PortModel>(_object); + const Atom& value = model->get_property(uris.ingen_value); + if (value.is_valid()) { + _app->set_property(_object->uri(), uris.lv2_maximum, value); + } +} + +void +PortMenu::on_menu_reset_range() +{ + const URIs& uris = _app->uris(); + SPtr<const PortModel> model = dynamic_ptr_cast<const PortModel>(_object); + + // Remove lv2:minimum and lv2:maximum properties + Properties remove; + remove.insert({uris.lv2_minimum, Property(uris.patch_wildcard)}); + remove.insert({uris.lv2_maximum, Property(uris.patch_wildcard)}); + _app->interface()->delta(_object->uri(), remove, Properties()); +} + +void +PortMenu::on_menu_expose() +{ + const URIs& uris = _app->uris(); + SPtr<const PortModel> port = dynamic_ptr_cast<const PortModel>(_object); + SPtr<const BlockModel> block = dynamic_ptr_cast<const BlockModel>(port->parent()); + + const std::string label = block->label() + " " + block->port_label(port); + const Raul::Path path = Raul::Path(block->path() + Raul::Symbol("_" + port->symbol())); + + Ingen::Resource r(*_object.get()); + r.remove_property(uris.lv2_index, uris.patch_wildcard); + r.set_property(uris.lv2_symbol, _app->forge().alloc(path.symbol())); + r.set_property(uris.lv2_name, _app->forge().alloc(label.c_str())); + + // TODO: Pretty kludgey coordinates + const float block_x = block->get_property(uris.ingen_canvasX).get<float>(); + const float block_y = block->get_property(uris.ingen_canvasY).get<float>(); + const float x_off = (label.length() * 16.0f) * (port->is_input() ? -1 : 1); + const float y_off = port->index() * 32.0f; + r.set_property(uris.ingen_canvasX, _app->forge().make(block_x + x_off)); + r.set_property(uris.ingen_canvasY, _app->forge().make(block_y + y_off)); + + _app->interface()->put(path_to_uri(path), r.properties()); + + if (port->is_input()) { + _app->interface()->connect(path, _object->path()); + } else { + _app->interface()->connect(_object->path(), path); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/PortMenu.hpp b/src/gui/PortMenu.hpp new file mode 100644 index 00000000..db567980 --- /dev/null +++ b/src/gui/PortMenu.hpp @@ -0,0 +1,66 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_PORTMENU_HPP +#define INGEN_GUI_PORTMENU_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/menu.h> +#include <gtkmm/menushell.h> + +#include "ingen/client/PortModel.hpp" +#include "ingen/types.hpp" + +#include "ObjectMenu.hpp" + +namespace Ingen { +namespace GUI { + +/** Menu for a Port. + * + * \ingroup GUI + */ +class PortMenu : public ObjectMenu +{ +public: + PortMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app, + SPtr<const Client::PortModel> port, + bool internal_graph_port = false); + +private: + void on_menu_disconnect(); + void on_menu_set_min(); + void on_menu_set_max(); + void on_menu_reset_range(); + void on_menu_expose(); + + Gtk::Menu* _port_menu; + Gtk::MenuItem* _set_min_menuitem; + Gtk::MenuItem* _set_max_menuitem; + Gtk::MenuItem* _reset_range_menuitem; + Gtk::MenuItem* _expose_menuitem; + + /// True iff this is a (flipped) port on a GraphPortModule in its graph + bool _internal_graph_port; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_PORTMENU_HPP diff --git a/src/gui/PropertiesWindow.cpp b/src/gui/PropertiesWindow.cpp new file mode 100644 index 00000000..4d47b3ae --- /dev/null +++ b/src/gui/PropertiesWindow.cpp @@ -0,0 +1,591 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <algorithm> +#include <cassert> +#include <set> + +#include <gtkmm/label.h> +#include <gtkmm/spinbutton.h> + +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/PluginModel.hpp" + +#include "App.hpp" +#include "PropertiesWindow.hpp" +#include "RDFS.hpp" +#include "URIEntry.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +typedef std::set<URI> URISet; + +PropertiesWindow::PropertiesWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) + , _value_type(0) +{ + xml->get_widget("properties_vbox", _vbox); + xml->get_widget("properties_scrolledwindow", _scrolledwindow); + xml->get_widget("properties_table", _table); + xml->get_widget("properties_key_combo", _key_combo); + xml->get_widget("properties_value_bin", _value_bin); + xml->get_widget("properties_add_button", _add_button); + xml->get_widget("properties_cancel_button", _cancel_button); + xml->get_widget("properties_apply_button", _apply_button); + xml->get_widget("properties_ok_button", _ok_button); + + _key_store = Gtk::ListStore::create(_combo_columns); + _key_combo->set_model(_key_store); + _key_combo->pack_start(_combo_columns.label_col); + + _key_combo->signal_changed().connect( + sigc::mem_fun(this, &PropertiesWindow::key_changed)); + + _add_button->signal_clicked().connect( + sigc::mem_fun(this, &PropertiesWindow::add_clicked)); + + _cancel_button->signal_clicked().connect( + sigc::mem_fun(this, &PropertiesWindow::cancel_clicked)); + + _apply_button->signal_clicked().connect( + sigc::mem_fun(this, &PropertiesWindow::apply_clicked)); + + _ok_button->signal_clicked().connect( + sigc::mem_fun(this, &PropertiesWindow::ok_clicked)); +} + +void +PropertiesWindow::reset() +{ + _property_connection.disconnect(); + _property_removed_connection.disconnect(); + + _key_store->clear(); + _records.clear(); + + _model.reset(); + + _table->children().clear(); + _table->resize(1, 3); + _table->property_n_rows() = 1; +} + +void +PropertiesWindow::present(SPtr<const ObjectModel> model) +{ + set_object(model); + Gtk::Window::present(); +} + +void +PropertiesWindow::add_property(const URI& key, const Atom& value) +{ + World* world = _app->world(); + + const unsigned n_rows = _table->property_n_rows() + 1; + _table->property_n_rows() = n_rows; + + // Column 0: Property + LilvNode* prop = lilv_new_uri(world->lilv_world(), key.c_str()); + std::string name = RDFS::label(world, prop); + if (name.empty()) { + name = world->rdf_world()->prefixes().qualify(key); + } + Gtk::Label* label = new Gtk::Label( + std::string("<a href=\"") + key.string() + "\">" + name + "</a>", + 1.0, + 0.5); + label->set_use_markup(true); + _app->set_tooltip(label, prop); + _table->attach(*Gtk::manage(label), 0, 1, n_rows, n_rows + 1, + Gtk::FILL|Gtk::SHRINK, Gtk::SHRINK); + + // Column 1: Value + Gtk::Alignment* align = manage(new Gtk::Alignment(0.0, 0.5, 1.0, 1.0)); + Gtk::CheckButton* present = manage(new Gtk::CheckButton()); + const char* type = _app->world()->uri_map().unmap_uri(value.type()); + Gtk::Widget* val_widget = create_value_widget(key, type, value); + + present->set_active(); + if (val_widget) { + align->add(*Gtk::manage(val_widget)); + _app->set_tooltip(val_widget, prop); + } + + _table->attach(*align, 1, 2, n_rows, n_rows + 1, + Gtk::FILL|Gtk::EXPAND, Gtk::SHRINK); + _table->attach(*present, 2, 3, n_rows, n_rows + 1, + Gtk::FILL, Gtk::SHRINK); + _records.emplace(key, Record(value, align, n_rows, present)); + _table->show_all(); + + lilv_node_free(prop); +} + +bool +PropertiesWindow::datatype_supported(const RDFS::URISet& types, + URI* widget_type) +{ + if (types.find(_app->uris().atom_Int) != types.end()) { + *widget_type = _app->uris().atom_Int; + return true; + } else if (types.find(_app->uris().atom_Float) != types.end()) { + *widget_type = _app->uris().atom_Float; + return true; + } else if (types.find(_app->uris().atom_Bool) != types.end()) { + *widget_type = _app->uris().atom_Bool; + return true; + } else if (types.find(_app->uris().atom_String) != types.end()) { + *widget_type = _app->uris().atom_String; + return true; + } else if (types.find(_app->uris().atom_URID) != types.end()) { + *widget_type = _app->uris().atom_URID; + return true; + } + + return false; +} + +bool +PropertiesWindow::class_supported(const RDFS::URISet& types) +{ + World* world = _app->world(); + LilvNode* rdf_type = lilv_new_uri( + world->lilv_world(), LILV_NS_RDF "type"); + + for (const auto& t : types) { + LilvNode* range = lilv_new_uri(world->lilv_world(), t.c_str()); + LilvNodes* instances = lilv_world_find_nodes( + world->lilv_world(), nullptr, rdf_type, range); + + const bool has_instance = (lilv_nodes_size(instances) > 0); + + lilv_nodes_free(instances); + lilv_node_free(range); + if (has_instance) { + lilv_node_free(rdf_type); + return true; + } + } + + lilv_node_free(rdf_type); + return false; +} + +/** Set the node this window is associated with. + * This function MUST be called before using this object in any way. + */ +void +PropertiesWindow::set_object(SPtr<const ObjectModel> model) +{ + reset(); + _model = model; + + set_title(model->path() + " Properties - Ingen"); + + World* world = _app->world(); + + LilvNode* rdf_type = lilv_new_uri( + world->lilv_world(), LILV_NS_RDF "type"); + LilvNode* rdfs_DataType = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "Datatype"); + + // Populate key combo + const URISet props = RDFS::properties(world, model); + std::map<std::string, URI> entries; + for (const auto& p : props) { + LilvNode* prop = lilv_new_uri(world->lilv_world(), p.c_str()); + const std::string label = RDFS::label(world, prop); + URISet ranges = RDFS::range(world, prop, true); + + lilv_node_free(prop); + if (label.empty() || ranges.empty()) { + // Property has no label or range, can't show a widget for it + continue; + } + + LilvNode* range = lilv_new_uri(world->lilv_world(), (*ranges.begin()).c_str()); + if (RDFS::is_a(world, range, rdfs_DataType)) { + // Range is a datatype, show if type or any subtype is supported + RDFS::datatypes(_app->world(), ranges, false); + URI widget_type("urn:nothing"); + if (datatype_supported(ranges, &widget_type)) { + entries.emplace(label, p); + } + } else { + // Range is presumably a class, show if any instances are known + if (class_supported(ranges)) { + entries.emplace(label, p); + } + } + } + + for (const auto& e : entries) { + Gtk::ListStore::iterator ki = _key_store->append(); + Gtk::ListStore::Row row = *ki; + row[_combo_columns.uri_col] = e.second.string(); + row[_combo_columns.label_col] = e.first; + } + + lilv_node_free(rdfs_DataType); + lilv_node_free(rdf_type); + + for (const auto& p : model->properties()) { + add_property(p.first, p.second); + } + + _table->show_all(); + + _property_connection = model->signal_property().connect( + sigc::mem_fun(this, &PropertiesWindow::add_property)); + _property_removed_connection = model->signal_property_removed().connect( + sigc::mem_fun(this, &PropertiesWindow::remove_property)); +} + +Gtk::Widget* +PropertiesWindow::create_value_widget(const URI& key, + const char* type_uri, + const Atom& value) +{ + if (!type_uri || !URI::is_valid(type_uri)) { + return nullptr; + } + + URI type(type_uri); + Ingen::World* world = _app->world(); + LilvWorld* lworld = world->lilv_world(); + + // See if type is a datatype we support + std::set<URI> types{type}; + RDFS::datatypes(_app->world(), types, false); + + URI widget_type("urn:nothing"); + const bool supported = datatype_supported(types, &widget_type); + if (supported) { + type = widget_type; + _value_type = _app->world()->uri_map().map_uri(type); + } + + if (type == _app->uris().atom_Int) { + Gtk::SpinButton* widget = manage(new Gtk::SpinButton(0.0, 0)); + widget->property_numeric() = true; + widget->set_range(INT_MIN, INT_MAX); + widget->set_increments(1, 10); + if (value.is_valid()) { + widget->set_value(value.get<int32_t>()); + } + widget->signal_value_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + return widget; + } else if (type == _app->uris().atom_Float) { + Gtk::SpinButton* widget = manage(new Gtk::SpinButton(0.0, 4)); + widget->property_numeric() = true; + widget->set_snap_to_ticks(false); + widget->set_range(-FLT_MAX, FLT_MAX); + widget->set_increments(0.1, 1.0); + if (value.is_valid()) { + widget->set_value(value.get<float>()); + } + widget->signal_value_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + return widget; + } else if (type == _app->uris().atom_Bool) { + Gtk::CheckButton* widget = manage(new Gtk::CheckButton()); + if (value.is_valid()) { + widget->set_active(value.get<int32_t>()); + } + widget->signal_toggled().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + return widget; + } else if (type == _app->uris().atom_String) { + Gtk::Entry* widget = manage(new Gtk::Entry()); + if (value.is_valid()) { + widget->set_text(value.ptr<char>()); + } + widget->signal_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + return widget; + } else if (type == _app->uris().atom_URID) { + const char* str = (value.is_valid() + ? world->uri_map().unmap_uri(value.get<int32_t>()) + : ""); + + LilvNode* pred = lilv_new_uri(lworld, key.c_str()); + URISet ranges = RDFS::range(world, pred, true); + URIEntry* widget = manage(new URIEntry(_app, ranges, str ? str : "")); + widget->signal_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + lilv_node_free(pred); + return widget; + } + + LilvNode* type_node = lilv_new_uri(lworld, type.c_str()); + LilvNode* rdfs_Class = lilv_new_uri(lworld, LILV_NS_RDFS "Class"); + const bool is_class = RDFS::is_a(world, type_node, rdfs_Class); + lilv_node_free(rdfs_Class); + lilv_node_free(type_node); + + if (type == _app->uris().atom_URI || + type == _app->uris().rdfs_Class || + is_class) { + LilvNode* pred = lilv_new_uri(lworld, key.c_str()); + URISet ranges = RDFS::range(world, pred, true); + const char* str = value.is_valid() ? value.ptr<const char>() : ""; + URIEntry* widget = manage(new URIEntry(_app, ranges, str)); + widget->signal_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + lilv_node_free(pred); + return widget; + } + + _app->log().error(fmt("No widget for value type %1%\n") % type); + + return nullptr; +} + +void +PropertiesWindow::on_show() +{ + static const int WIN_PAD = 64; + static const int VBOX_PAD = 16; + + int width = 0; + int height = 0; + + for (const auto& c : _vbox->children()) { + const Gtk::Requisition& req = c.get_widget()->size_request(); + + width = std::max(width, req.width); + height += req.height + VBOX_PAD; + } + + const Gtk::Requisition& req = _table->size_request(); + + width = 1.2 * std::max(width, req.width + 128); + height += req.height; + + set_default_size(width + WIN_PAD, height + WIN_PAD); + resize(width + WIN_PAD, height + WIN_PAD); + Gtk::Window::on_show(); +} + +void +PropertiesWindow::change_property(const URI& key, const Atom& value) +{ + auto r = _records.find(key); + if (r == _records.end()) { + add_property(key, value); + _table->show_all(); + return; + } + + Record& record = r->second; + const char* type = _app->world()->uri_map().unmap_uri(value.type()); + Gtk::Widget* val_widget = create_value_widget(key, type, value); + + if (val_widget) { + record.value_widget->remove(); + record.value_widget->add(*Gtk::manage(val_widget)); + val_widget->show_all(); + } + + record.value = value; +} + +void +PropertiesWindow::remove_property(const URI& key, const Atom& value) +{ + // Bleh, there doesn't seem to be an easy way to remove a Gtk::Table row... + _records.clear(); + _table->children().clear(); + _table->resize(1, 3); + _table->property_n_rows() = 1; + + for (const auto& p : _model->properties()) { + add_property(p.first, p.second); + } + _table->show_all(); +} + +Atom +PropertiesWindow::get_value(LV2_URID type, Gtk::Widget* value_widget) +{ + Forge& forge = _app->forge(); + + if (type == forge.Int) { + Gtk::SpinButton* spin = dynamic_cast<Gtk::SpinButton*>(value_widget); + if (spin) { + return _app->forge().make(spin->get_value_as_int()); + } + } else if (type == forge.Float) { + Gtk::SpinButton* spin = dynamic_cast<Gtk::SpinButton*>(value_widget); + if (spin) { + return _app->forge().make(static_cast<float>(spin->get_value())); + } + } else if (type == forge.Bool) { + Gtk::CheckButton* check = dynamic_cast<Gtk::CheckButton*>(value_widget); + if (check) { + return _app->forge().make(check->get_active()); + } + } else if (type == forge.URI || type == forge.URID) { + URIEntry* uri_entry = dynamic_cast<URIEntry*>(value_widget); + if (uri_entry && URI::is_valid(uri_entry->get_text())) { + return _app->forge().make_urid(URI(uri_entry->get_text())); + } else { + _app->log().error(fmt("Invalid URI <%1%>\n") % uri_entry->get_text()); + } + } else if (type == forge.String) { + Gtk::Entry* entry = dynamic_cast<Gtk::Entry*>(value_widget); + if (entry) { + return _app->forge().alloc(entry->get_text()); + } + } + + return Atom(); +} + +void +PropertiesWindow::on_change(const URI& key) +{ + auto r = _records.find(key); + if (r == _records.end()) { + return; + } + + Record& record = r->second; + const Atom value = get_value(record.value.type(), + record.value_widget->get_child()); + + if (value.is_valid()) { + record.value = value; + } else { + _app->log().error(fmt("Failed to get `%1%' value from widget\n") % key); + } +} + +std::string +PropertiesWindow::active_key() const +{ + const Gtk::ListStore::iterator iter = _key_combo->get_active(); + if (!iter) { + return ""; + } + + Glib::ustring prop_uri = (*iter)[_combo_columns.uri_col]; + return prop_uri; +} + +void +PropertiesWindow::key_changed() +{ + _value_bin->remove(); + if (!_key_combo->get_active()) { + return; + } + + LilvWorld* lworld = _app->world()->lilv_world(); + const Gtk::ListStore::Row key_row = *(_key_combo->get_active()); + const Glib::ustring key_uri = key_row[_combo_columns.uri_col]; + LilvNode* prop = lilv_new_uri(lworld, key_uri.c_str()); + + // Try to create a value widget in the range of this property + const URISet ranges = RDFS::range(_app->world(), prop, true); + for (const auto& r : ranges) { + Gtk::Widget* value_widget = create_value_widget( + URI(key_uri), r.c_str(), Atom()); + + if (value_widget) { + _add_button->set_sensitive(true); + _value_bin->remove(); + _value_bin->add(*Gtk::manage(value_widget)); + _value_bin->show_all(); + break; + } + } + + lilv_node_free(prop); +} + +void +PropertiesWindow::add_clicked() +{ + if (!_key_combo->get_active() || !_value_type || !_value_bin->get_child()) { + return; + } + + // Get selected key URI + const Gtk::ListStore::Row key_row = *(_key_combo->get_active()); + const Glib::ustring key_uri = key_row[_combo_columns.uri_col]; + + // Try to get value from value widget + const Atom& value = get_value(_value_type, _value_bin->get_child()); + if (value.is_valid()) { + // Send property to engine + Properties properties; + properties.emplace(URI(key_uri.c_str()), Property(value)); + _app->interface()->put(_model->uri(), properties); + } +} + +void +PropertiesWindow::cancel_clicked() +{ + reset(); + Gtk::Window::hide(); +} + +void +PropertiesWindow::apply_clicked() +{ + Properties remove; + Properties add; + for (const auto& r : _records) { + const URI& uri = r.first; + const Record& record = r.second; + if (record.present_button->get_active()) { + if (!_model->has_property(uri, record.value)) { + add.emplace(uri, record.value); + } + } else { + remove.emplace(uri, record.value); + } + } + + if (remove.empty()) { + _app->interface()->put(_model->uri(), add); + } else { + _app->interface()->delta(_model->uri(), remove, add); + } +} + +void +PropertiesWindow::ok_clicked() +{ + apply_clicked(); + Gtk::Window::hide(); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/PropertiesWindow.hpp b/src/gui/PropertiesWindow.hpp new file mode 100644 index 00000000..f4a8dd0d --- /dev/null +++ b/src/gui/PropertiesWindow.hpp @@ -0,0 +1,129 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_PROPERTIES_WINDOW_HPP +#define INGEN_GUI_PROPERTIES_WINDOW_HPP + +#include <map> + +#include <gtkmm/alignment.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/table.h> + +#include "ingen/client/BlockModel.hpp" +#include "ingen/types.hpp" + +#include "Window.hpp" + +namespace Ingen { + +namespace Client { class ObjectModel; } + +namespace GUI { + +/** Object properties window. + * + * Loaded from XML as a derived object. + * + * \ingroup GUI + */ +class PropertiesWindow : public Window +{ +public: + PropertiesWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void present(SPtr<const Client::ObjectModel> model); + void set_object(SPtr<const Client::ObjectModel> model); + +private: + /** Record of a property (row in the table) */ + struct Record { + Record(const Atom& v, Gtk::Alignment* vw, int r, Gtk::CheckButton* cb) + : value(v), value_widget(vw), row(r), present_button(cb) + {} + Atom value; + Gtk::Alignment* value_widget; + int row; + Gtk::CheckButton* present_button; + }; + + struct ComboColumns : public Gtk::TreeModel::ColumnRecord { + ComboColumns() { + add(label_col); + add(uri_col); + } + Gtk::TreeModelColumn<Glib::ustring> label_col; + Gtk::TreeModelColumn<Glib::ustring> uri_col; + }; + + void add_property(const URI& key, const Atom& value); + void change_property(const URI& key, const Atom& value); + void remove_property(const URI& key, const Atom& value); + void on_change(const URI& key); + + bool datatype_supported(const std::set<URI>& types, + URI* widget_type); + + bool class_supported(const std::set<URI>& types); + + Gtk::Widget* create_value_widget(const URI& key, + const char* type_uri, + const Atom& value = Atom()); + + Atom get_value(LV2_URID type, Gtk::Widget* value_widget); + + void reset(); + void on_show(); + + std::string active_key() const; + + void key_changed(); + void add_clicked(); + void cancel_clicked(); + void apply_clicked(); + void ok_clicked(); + + typedef std::map<URI, Record> Records; + Records _records; + + SPtr<const Client::ObjectModel> _model; + ComboColumns _combo_columns; + Glib::RefPtr<Gtk::ListStore> _key_store; + sigc::connection _property_connection; + sigc::connection _property_removed_connection; + Gtk::VBox* _vbox; + Gtk::ScrolledWindow* _scrolledwindow; + Gtk::Table* _table; + Gtk::ComboBox* _key_combo; + LV2_URID _value_type; + Gtk::Bin* _value_bin; + Gtk::Button* _add_button; + Gtk::Button* _cancel_button; + Gtk::Button* _apply_button; + Gtk::Button* _ok_button; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_PROPERTIES_WINDOW_HPP diff --git a/src/gui/RDFS.cpp b/src/gui/RDFS.cpp new file mode 100644 index 00000000..71b3441a --- /dev/null +++ b/src/gui/RDFS.cpp @@ -0,0 +1,259 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/Log.hpp" +#include "ingen/Resource.hpp" +#include "ingen/World.hpp" +#include "ingen/client/ObjectModel.hpp" +#include "lilv/lilv.h" + +#include "RDFS.hpp" + +namespace Ingen { +namespace GUI { +namespace RDFS { + +std::string +label(World* world, const LilvNode* node) +{ + LilvNode* rdfs_label = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "label"); + LilvNodes* labels = lilv_world_find_nodes( + world->lilv_world(), node, rdfs_label, nullptr); + + const LilvNode* first = lilv_nodes_get_first(labels); + std::string label = first ? lilv_node_as_string(first) : ""; + + lilv_nodes_free(labels); + lilv_node_free(rdfs_label); + return label; +} + +std::string +comment(World* world, const LilvNode* node) +{ + LilvNode* rdfs_comment = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "comment"); + LilvNodes* comments = lilv_world_find_nodes( + world->lilv_world(), node, rdfs_comment, nullptr); + + const LilvNode* first = lilv_nodes_get_first(comments); + std::string comment = first ? lilv_node_as_string(first) : ""; + + lilv_nodes_free(comments); + lilv_node_free(rdfs_comment); + return comment; +} + +static void +closure(World* world, const LilvNode* pred, URISet& types, bool super) +{ + unsigned added = 0; + do { + added = 0; + URISet klasses; + for (const auto& t : types) { + LilvNode* type = lilv_new_uri(world->lilv_world(), t.c_str()); + LilvNodes* matches = (super) + ? lilv_world_find_nodes( + world->lilv_world(), type, pred, nullptr) + : lilv_world_find_nodes( + world->lilv_world(), nullptr, pred, type); + LILV_FOREACH(nodes, m, matches) { + const LilvNode* klass_node = lilv_nodes_get(matches, m); + if (lilv_node_is_uri(klass_node)) { + URI klass(lilv_node_as_uri(klass_node)); + if (!types.count(klass)) { + ++added; + klasses.insert(klass); + } + } + } + lilv_nodes_free(matches); + lilv_node_free(type); + } + types.insert(klasses.begin(), klasses.end()); + } while (added > 0); +} + +void +classes(World* world, URISet& types, bool super) +{ + LilvNode* rdfs_subClassOf = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "subClassOf"); + + closure(world, rdfs_subClassOf, types, super); + + lilv_node_free(rdfs_subClassOf); +} + +void +datatypes(World* world, URISet& types, bool super) +{ + LilvNode* owl_onDatatype = lilv_new_uri( + world->lilv_world(), LILV_NS_OWL "onDatatype"); + + closure(world, owl_onDatatype, types, super); + + lilv_node_free(owl_onDatatype); +} + +URISet +types(World* world, SPtr<const Client::ObjectModel> model) +{ + typedef Properties::const_iterator PropIter; + typedef std::pair<PropIter, PropIter> PropRange; + + // Start with every rdf:type + URISet types; + types.insert(URI(LILV_NS_RDFS "Resource")); + PropRange range = model->properties().equal_range(world->uris().rdf_type); + for (auto t = range.first; t != range.second; ++t) { + if (t->second.type() == world->forge().URI || + t->second.type() == world->forge().URID) { + const URI type(world->forge().str(t->second, false)); + types.insert(type); + if (world->uris().ingen_Graph == type) { + // Add lv2:Plugin as a type for graphs so plugin properties show up + types.insert(world->uris().lv2_Plugin); + } + } else { + world->log().error(fmt("<%1%> has non-URI type\n") % model->uri()); + } + } + + // Add every superclass of every type, recursively + RDFS::classes(world, types, true); + + return types; +} + +URISet +properties(World* world, SPtr<const Client::ObjectModel> model) +{ + URISet properties; + URISet types = RDFS::types(world, model); + + LilvNode* rdf_type = lilv_new_uri(world->lilv_world(), + LILV_NS_RDF "type"); + LilvNode* rdf_Property = lilv_new_uri(world->lilv_world(), + LILV_NS_RDF "Property"); + LilvNode* rdfs_domain = lilv_new_uri(world->lilv_world(), + LILV_NS_RDFS "domain"); + + LilvNodes* props = lilv_world_find_nodes( + world->lilv_world(), nullptr, rdf_type, rdf_Property); + LILV_FOREACH(nodes, p, props) { + const LilvNode* prop = lilv_nodes_get(props, p); + if (lilv_node_is_uri(prop)) { + LilvNodes* domains = lilv_world_find_nodes( + world->lilv_world(), prop, rdfs_domain, nullptr); + unsigned n_matching_domains = 0; + LILV_FOREACH(nodes, d, domains) { + const LilvNode* domain_node = lilv_nodes_get(domains, d); + if (!lilv_node_is_uri(domain_node)) { + // TODO: Blank node domains (e.g. unions) + continue; + } + + const URI domain(lilv_node_as_uri(domain_node)); + if (types.count(domain)) { + ++n_matching_domains; + } + } + + if (lilv_nodes_size(domains) == 0 || ( + n_matching_domains > 0 && + n_matching_domains == lilv_nodes_size(domains))) { + properties.insert(URI(lilv_node_as_uri(prop))); + } + + lilv_nodes_free(domains); + } + } + + lilv_node_free(rdfs_domain); + lilv_node_free(rdf_Property); + lilv_node_free(rdf_type); + + return properties; +} + +Objects +instances(World* world, const URISet& types) +{ + LilvNode* rdf_type = lilv_new_uri( + world->lilv_world(), LILV_NS_RDF "type"); + + Objects result; + for (const auto& t : types) { + LilvNode* type = lilv_new_uri(world->lilv_world(), t.c_str()); + LilvNodes* objects = lilv_world_find_nodes( + world->lilv_world(), nullptr, rdf_type, type); + LILV_FOREACH(nodes, o, objects) { + const LilvNode* object = lilv_nodes_get(objects, o); + if (!lilv_node_is_uri(object)) { + continue; + } + const std::string label = RDFS::label(world, object); + result.emplace(label, URI(lilv_node_as_string(object))); + } + lilv_node_free(type); + } + + lilv_node_free(rdf_type); + return result; +} + +URISet +range(World* world, const LilvNode* prop, bool recursive) +{ + LilvNode* rdfs_range = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "range"); + + LilvNodes* nodes = lilv_world_find_nodes( + world->lilv_world(), prop, rdfs_range, nullptr); + + URISet ranges; + LILV_FOREACH(nodes, n, nodes) { + ranges.insert(URI(lilv_node_as_string(lilv_nodes_get(nodes, n)))); + } + + if (recursive) { + RDFS::classes(world, ranges, false); + } + + lilv_nodes_free(nodes); + lilv_node_free(rdfs_range); + return ranges; +} + +bool +is_a(World* world, const LilvNode* inst, const LilvNode* klass) +{ + LilvNode* rdf_type = lilv_new_uri(world->lilv_world(), LILV_NS_RDF "type"); + + const bool is_instance = lilv_world_ask( + world->lilv_world(), inst, rdf_type, klass); + + lilv_node_free(rdf_type); + return is_instance; +} + +} // namespace RDFS +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/RDFS.hpp b/src/gui/RDFS.hpp new file mode 100644 index 00000000..f59bbdf5 --- /dev/null +++ b/src/gui/RDFS.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_RDF_HPP +#define INGEN_GUI_RDF_HPP + +#include <map> +#include <set> +#include <string> + +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +namespace Ingen { + +class World; + +namespace Client { class ObjectModel; } + +namespace GUI { + +namespace RDFS { + +/** Set of URIs. */ +typedef std::set<URI> URISet; + +/** Label => Resource map. */ +typedef std::map<std::string, URI> Objects; + +/** Return the label of `node`. */ +std::string label(World* world, const LilvNode* node); + +/** Return the comment of `node`. */ +std::string comment(World* world, const LilvNode* node); + +/** Set `types` to its super/sub class closure. + * @param super If true, find all superclasses, otherwise all subclasses + */ +void classes(World* world, URISet& types, bool super); + +/** Set `types` to its super/sub datatype closure. + * @param super If true, find all supertypes, otherwise all subtypes. + */ +void datatypes(World* world, URISet& types, bool super); + +/** Get all instances of any class in `types`. */ +Objects instances(World* world, const URISet& types); + +/** Get all the types which `model` is an instance of. */ +URISet types(World* world, SPtr<const Client::ObjectModel> model); + +/** Get all the properties with domains appropriate for `model`. */ +URISet properties(World* world, SPtr<const Client::ObjectModel> model); + +/** Return the range (value types) of `prop`. + * @param recursive If true, include all subclasses. + */ +URISet range(World* world, const LilvNode* prop, bool recursive); + +/** Return true iff `inst` is-a `klass`. */ +bool is_a(World* world, const LilvNode* inst, const LilvNode* klass); + +} // namespace RDFS +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_RDF_HPP diff --git a/src/gui/RenameWindow.cpp b/src/gui/RenameWindow.cpp new file mode 100644 index 00000000..c83143d9 --- /dev/null +++ b/src/gui/RenameWindow.cpp @@ -0,0 +1,137 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> + +#include "ingen/Forge.hpp" +#include "ingen/Interface.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/ObjectModel.hpp" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +#include "App.hpp" +#include "RenameWindow.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +RenameWindow::RenameWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) +{ + xml->get_widget("rename_symbol_entry", _symbol_entry); + xml->get_widget("rename_label_entry", _label_entry); + xml->get_widget("rename_message_label", _message_label); + xml->get_widget("rename_cancel_button", _cancel_button); + xml->get_widget("rename_ok_button", _ok_button); + + _symbol_entry->signal_changed().connect( + sigc::mem_fun(this, &RenameWindow::values_changed)); + _label_entry->signal_changed().connect( + sigc::mem_fun(this, &RenameWindow::values_changed)); + _cancel_button->signal_clicked().connect( + sigc::mem_fun(this, &RenameWindow::cancel_clicked)); + _ok_button->signal_clicked().connect( + sigc::mem_fun(this, &RenameWindow::ok_clicked)); + + _ok_button->property_sensitive() = false; +} + +/** Set the object this window is renaming. + * This function MUST be called before using this object in any way. + */ +void +RenameWindow::set_object(SPtr<const ObjectModel> object) +{ + _object = object; + _symbol_entry->set_text(object->path().symbol()); + const Atom& name_atom = object->get_property(_app->uris().lv2_name); + _label_entry->set_text( + (name_atom.type() == _app->forge().String) ? name_atom.ptr<char>() : ""); +} + +void +RenameWindow::present(SPtr<const ObjectModel> object) +{ + set_object(object); + _symbol_entry->grab_focus(); + Gtk::Window::present(); +} + +void +RenameWindow::values_changed() +{ + const std::string& symbol = _symbol_entry->get_text(); + if (!Raul::Symbol::is_valid(symbol)) { + _message_label->set_text("Invalid symbol"); + _ok_button->property_sensitive() = false; + } else if (_object->symbol() != symbol && + _app->store()->object( + _object->parent()->path().child(Raul::Symbol(symbol)))) { + _message_label->set_text("An object already exists with that path"); + _ok_button->property_sensitive() = false; + } else { + _message_label->set_text(""); + _ok_button->property_sensitive() = true; + } +} + +void +RenameWindow::cancel_clicked() +{ + _symbol_entry->set_text(""); + hide(); +} + +/** Rename the object. + * + * It shouldn't be possible for this to be called with an invalid name set + * (since the Rename button should be deactivated). This is just shinification + * though - the engine will handle invalid names gracefully. + */ +void +RenameWindow::ok_clicked() +{ + const URIs& uris = _app->uris(); + const std::string& symbol_str = _symbol_entry->get_text(); + const std::string& label = _label_entry->get_text(); + Raul::Path path = _object->path(); + const Atom& name_atom = _object->get_property(uris.lv2_name); + + if (!label.empty() && (name_atom.type() != uris.forge.String || + label != name_atom.ptr<char>())) { + _app->set_property(path_to_uri(path), + uris.lv2_name, + _app->forge().alloc(label)); + } + + if (Raul::Symbol::is_valid(symbol_str)) { + const Raul::Symbol symbol(symbol_str); + if (symbol != _object->symbol()) { + path = _object->path().parent().child(symbol); + _app->interface()->move(_object->path(), path); + } + } + + hide(); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/RenameWindow.hpp b/src/gui/RenameWindow.hpp new file mode 100644 index 00000000..36264879 --- /dev/null +++ b/src/gui/RenameWindow.hpp @@ -0,0 +1,64 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_RENAMEWINDOW_HPP +#define INGEN_GUI_RENAMEWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/label.h> + +#include "ingen/client/ObjectModel.hpp" +#include "ingen/types.hpp" + +#include "Window.hpp" + +namespace Ingen { +namespace GUI { + +/** Rename window. Handles renaming of any (Ingen) object. + * + * \ingroup GUI + */ +class RenameWindow : public Window +{ +public: + RenameWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void present(SPtr<const Client::ObjectModel> object); + +private: + void set_object(SPtr<const Client::ObjectModel> object); + + void values_changed(); + void cancel_clicked(); + void ok_clicked(); + + SPtr<const Client::ObjectModel> _object; + + Gtk::Entry* _symbol_entry; + Gtk::Entry* _label_entry; + Gtk::Label* _message_label; + Gtk::Button* _cancel_button; + Gtk::Button* _ok_button; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_RENAMEWINDOW_HPP diff --git a/src/gui/Style.cpp b/src/gui/Style.cpp new file mode 100644 index 00000000..81e6fb6c --- /dev/null +++ b/src/gui/Style.cpp @@ -0,0 +1,106 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cstdlib> +#include <fstream> +#include <map> +#include <string> + +#include "ganv/Port.hpp" +#include "ingen/Log.hpp" +#include "ingen/Parser.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/client/PortModel.hpp" + +#include "App.hpp" +#include "Style.hpp" +#include "Port.hpp" + +namespace Ingen { +namespace GUI { + +using namespace Ingen::Client; + +Style::Style(App& app) + // Colours from the Tango palette with modified V + : _app(app) +#ifdef INGEN_USE_LIGHT_THEME + , _audio_port_color(0xC8E6ABFF) // Green + , _control_port_color(0xAAC0E6FF) // Blue + , _cv_port_color(0xACE6E0FF) // Teal (between audio and control) + , _event_port_color(0xE6ABABFF) // Red + , _string_port_color(0xD8ABE6FF) // Plum +#else + , _audio_port_color(0x4A8A0EFF) // Green + , _control_port_color(0x244678FF) // Blue + , _cv_port_color(0x248780FF) // Teal (between audio and control) + , _event_port_color(0x960909FF) // Red + , _string_port_color(0x5C3566FF) // Plum +#endif +{ +} + +/** Loads settings from the rc file. Passing no parameter will load from + * the default location. + */ +void +Style::load_settings(std::string filename) +{ + /* ... */ +} + +/** Saves settings to rc file. Passing no parameter will save to the + * default location. + */ +void +Style::save_settings(std::string filename) +{ + /* ... */ +} + +/** Applies the current loaded settings to whichever parts of the app + * need updating. + */ +void +Style::apply_settings() +{ + /* ... */ +} + +uint32_t +Style::get_port_color(const Client::PortModel* p) +{ + const URIs& uris = _app.uris(); + if (p->is_a(uris.lv2_AudioPort)) { + return _audio_port_color; + } else if (p->is_a(uris.lv2_ControlPort)) { + return _control_port_color; + } else if (p->is_a(uris.lv2_CVPort)) { + return _cv_port_color; + } else if (p->supports(uris.atom_String)) { + return _string_port_color; + } else if (_app.can_control(p)) { + return _control_port_color; + } else if (p->is_a(uris.atom_AtomPort)) { + return _event_port_color; + } + + return 0x555555FF; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/Style.hpp b/src/gui/Style.hpp new file mode 100644 index 00000000..8e628a3d --- /dev/null +++ b/src/gui/Style.hpp @@ -0,0 +1,56 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_STYLE_HPP +#define INGEN_GUI_STYLE_HPP + +#include <cstdint> +#include <string> + +namespace Ingen { namespace Client { class PortModel; } } + +namespace Ingen { +namespace GUI { + +class App; +class Port; + +class Style +{ +public: + explicit Style(App& app); + + void load_settings(std::string filename = ""); + void save_settings(std::string filename = ""); + + void apply_settings(); + + uint32_t get_port_color(const Client::PortModel* p); + +private: + App& _app; + + uint32_t _audio_port_color; + uint32_t _control_port_color; + uint32_t _cv_port_color; + uint32_t _event_port_color; + uint32_t _string_port_color; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_STYLE_HPP diff --git a/src/gui/SubgraphModule.cpp b/src/gui/SubgraphModule.cpp new file mode 100644 index 00000000..6bbcf534 --- /dev/null +++ b/src/gui/SubgraphModule.cpp @@ -0,0 +1,102 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <utility> + +#include "ingen/Interface.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "NodeModule.hpp" +#include "GraphCanvas.hpp" +#include "GraphWindow.hpp" +#include "Port.hpp" +#include "SubgraphModule.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +SubgraphModule::SubgraphModule(GraphCanvas& canvas, + SPtr<const GraphModel> graph) + : NodeModule(canvas, graph) + , _graph(graph) +{ + assert(graph); +} + +bool +SubgraphModule::on_double_click(GdkEventButton* event) +{ + assert(_graph); + + SPtr<GraphModel> parent = dynamic_ptr_cast<GraphModel>(_graph->parent()); + + GraphWindow* const preferred = ( (parent && (event->state & GDK_SHIFT_MASK)) + ? nullptr + : app().window_factory()->graph_window(parent) ); + + app().window_factory()->present_graph(_graph, preferred); + return true; +} + +void +SubgraphModule::store_location(double ax, double ay) +{ + const URIs& uris = app().uris(); + + const Atom x(app().forge().make(static_cast<float>(ax))); + const Atom y(app().forge().make(static_cast<float>(ay))); + + if (x != _block->get_property(uris.ingen_canvasX) || + y != _block->get_property(uris.ingen_canvasY)) + { + app().interface()->put(_graph->uri(), + {{uris.ingen_canvasX, x}, + {uris.ingen_canvasY, y}}, + Resource::Graph::EXTERNAL); + } +} + +/** Browse to this graph in current (parent's) window + * (unless an existing window is displaying it) + */ +void +SubgraphModule::browse_to_graph() +{ + assert(_graph->parent()); + + SPtr<GraphModel> parent = dynamic_ptr_cast<GraphModel>(_graph->parent()); + + GraphWindow* const preferred = (parent) + ? app().window_factory()->graph_window(parent) + : nullptr; + + app().window_factory()->present_graph(_graph, preferred); +} + +void +SubgraphModule::menu_remove() +{ + app().interface()->del(_graph->uri()); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/SubgraphModule.hpp b/src/gui/SubgraphModule.hpp new file mode 100644 index 00000000..1b8df2fa --- /dev/null +++ b/src/gui/SubgraphModule.hpp @@ -0,0 +1,64 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_SUBGRAPHMODULE_HPP +#define INGEN_GUI_SUBGRAPHMODULE_HPP + +#include "ingen/types.hpp" + +#include "NodeModule.hpp" +#include "GraphPortModule.hpp" + +namespace Ingen { namespace Client { +class GraphModel; +class GraphWindow; +class PortModel; +} } + +namespace Ingen { +namespace GUI { + +class GraphCanvas; + +/** A module to represent a subgraph + * + * \ingroup GUI + */ +class SubgraphModule : public NodeModule +{ +public: + SubgraphModule(GraphCanvas& canvas, + SPtr<const Client::GraphModel> graph); + + virtual ~SubgraphModule() {} + + bool on_double_click(GdkEventButton* event); + + void store_location(double ax, double ay); + + void browse_to_graph(); + void menu_remove(); + + SPtr<const Client::GraphModel> graph() const { return _graph; } + +protected: + SPtr<const Client::GraphModel> _graph; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_SUBGRAPHMODULE_HPP diff --git a/src/gui/ThreadedLoader.cpp b/src/gui/ThreadedLoader.cpp new file mode 100644 index 00000000..7a80fa6e --- /dev/null +++ b/src/gui/ThreadedLoader.cpp @@ -0,0 +1,148 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> + +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/World.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "ThreadedLoader.hpp" + +using boost::optional; + +namespace Ingen { +namespace GUI { + +ThreadedLoader::ThreadedLoader(App& app, SPtr<Interface> engine) + : _app(app) + , _sem(0) + , _engine(std::move(engine)) + , _exit_flag(false) + , _thread(&ThreadedLoader::run, this) +{ + if (!parser()) { + app.log().warn("Parser unavailable, graph loading disabled\n"); + } +} + +ThreadedLoader::~ThreadedLoader() +{ + _exit_flag = true; + _sem.post(); + if (_thread.joinable()) { + _thread.join(); + } +} + +SPtr<Parser> +ThreadedLoader::parser() +{ + return _app.world()->parser(); +} + +void +ThreadedLoader::run() +{ + while (_sem.wait() && !_exit_flag) { + std::lock_guard<std::mutex> lock(_mutex); + while (!_events.empty()) { + _events.front()(); + _events.pop_front(); + } + } +} + +void +ThreadedLoader::load_graph(bool merge, + const FilePath& file_path, + optional<Raul::Path> engine_parent, + optional<Raul::Symbol> engine_symbol, + optional<Properties> engine_data) +{ + std::lock_guard<std::mutex> lock(_mutex); + + Glib::ustring engine_base = ""; + if (engine_parent) { + if (merge) { + engine_base = engine_parent.get(); + } else { + engine_base = engine_parent.get().base(); + } + } + + _events.push_back(sigc::hide_return( + sigc::bind(sigc::mem_fun(this, &ThreadedLoader::load_graph_event), + file_path, + engine_parent, + engine_symbol, + engine_data))); + + _sem.post(); +} + +void +ThreadedLoader::load_graph_event(const FilePath& file_path, + optional<Raul::Path> engine_parent, + optional<Raul::Symbol> engine_symbol, + optional<Properties> engine_data) +{ + std::lock_guard<std::mutex> lock(_app.world()->rdf_mutex()); + + _app.world()->parser()->parse_file(_app.world(), + _app.world()->interface().get(), + file_path, + engine_parent, + engine_symbol, + engine_data); +} + +void +ThreadedLoader::save_graph(SPtr<const Client::GraphModel> model, const URI& uri) +{ + std::lock_guard<std::mutex> lock(_mutex); + + _events.push_back(sigc::hide_return( + sigc::bind(sigc::mem_fun(this, &ThreadedLoader::save_graph_event), + model, + uri))); + + _sem.post(); +} + +void +ThreadedLoader::save_graph_event(SPtr<const Client::GraphModel> model, + const URI& uri) +{ + assert(uri.scheme() == "file"); + if (_app.serialiser()) { + std::lock_guard<std::mutex> lock(_app.world()->rdf_mutex()); + + if (uri.string().find(".ingen") != std::string::npos) { + _app.serialiser()->write_bundle(model, uri); + } else { + _app.serialiser()->start_to_file(model->path(), std::string(uri.path())); + _app.serialiser()->serialise(model); + _app.serialiser()->finish(); + } + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/ThreadedLoader.hpp b/src/gui/ThreadedLoader.hpp new file mode 100644 index 00000000..79ef6466 --- /dev/null +++ b/src/gui/ThreadedLoader.hpp @@ -0,0 +1,96 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_THREADEDLOADER_HPP +#define INGEN_GUI_THREADEDLOADER_HPP + +#include <thread> + +#include <cassert> +#include <list> +#include <mutex> +#include <string> + +#include <boost/optional.hpp> + +#include "ingen/FilePath.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Parser.hpp" +#include "ingen/Serialiser.hpp" +#include "raul/Semaphore.hpp" + +namespace Ingen { + +class URI; + +namespace GUI { + +/** Thread for loading graph files. + * + * This is a seperate thread so it can send all the loading message without + * blocking everything else, so the app can respond to the incoming events + * caused as a result of the graph loading, while the graph loads. + * + * Implemented as a slave with a list of closures (events) which processes + * all events in the (mutex protected) list each time it's whipped. + * + * \ingroup GUI + */ +class ThreadedLoader +{ +public: + ThreadedLoader(App& app, + SPtr<Interface> engine); + + ~ThreadedLoader(); + + void load_graph(bool merge, + const FilePath& file_path, + boost::optional<Raul::Path> engine_parent, + boost::optional<Raul::Symbol> engine_symbol, + boost::optional<Properties> engine_data); + + void save_graph(SPtr<const Client::GraphModel> model, const URI& uri); + + SPtr<Parser> parser(); + +private: + void load_graph_event(const FilePath& file_path, + boost::optional<Raul::Path> engine_parent, + boost::optional<Raul::Symbol> engine_symbol, + boost::optional<Properties> engine_data); + + void save_graph_event(SPtr<const Client::GraphModel> model, + const URI& filename); + + /** Returns nothing and takes no parameters (because they have all been bound) */ + typedef sigc::slot<void> Closure; + + void run(); + + App& _app; + Raul::Semaphore _sem; + SPtr<Interface> _engine; + std::mutex _mutex; + std::list<Closure> _events; + bool _exit_flag; + std::thread _thread; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_LOADERRTHREAD_HPP diff --git a/src/gui/URIEntry.cpp b/src/gui/URIEntry.cpp new file mode 100644 index 00000000..0b81afd7 --- /dev/null +++ b/src/gui/URIEntry.cpp @@ -0,0 +1,192 @@ +/* + This file is part of Ingen. + Copyright 2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <unordered_map> + +#include "App.hpp" +#include "RDFS.hpp" +#include "URIEntry.hpp" + +namespace Ingen { +namespace GUI { + +URIEntry::URIEntry(App* app, std::set<URI> types, const std::string& value) + : Gtk::HBox(false, 4) + , _app(app) + , _types(std::move(types)) + , _menu_button(Gtk::manage(new Gtk::Button("≡"))) + , _entry(Gtk::manage(new Gtk::Entry())) +{ + pack_start(*_entry, true, true); + pack_start(*_menu_button, false, true); + + _entry->set_text(value); + + _menu_button->signal_event().connect( + sigc::mem_fun(this, &URIEntry::menu_button_event)); +} + +Gtk::Menu* +URIEntry::build_value_menu() +{ + World* world = _app->world(); + LilvWorld* lworld = world->lilv_world(); + Gtk::Menu* menu = new Gtk::Menu(); + + LilvNode* owl_onDatatype = lilv_new_uri(lworld, LILV_NS_OWL "onDatatype"); + LilvNode* rdf_type = lilv_new_uri(lworld, LILV_NS_RDF "type"); + LilvNode* rdfs_Class = lilv_new_uri(lworld, LILV_NS_RDFS "Class"); + LilvNode* rdfs_Datatype = lilv_new_uri(lworld, LILV_NS_RDFS "Datatype"); + LilvNode* rdfs_subClassOf = lilv_new_uri(lworld, LILV_NS_RDFS "subClassOf"); + + RDFS::Objects values = RDFS::instances(world, _types); + + for (const auto& v : values) { + const LilvNode* inst = lilv_new_uri(lworld, v.second.c_str()); + std::string label = v.first; + if (label.empty()) { + // No label, show raw URI + label = lilv_node_as_string(inst); + } + + if (lilv_world_ask(world->lilv_world(), inst, rdf_type, rdfs_Class) || + lilv_world_ask(world->lilv_world(), inst, rdf_type, rdfs_Datatype)) { + // This value is a class or datatype... + if (!lilv_world_ask(lworld, inst, rdfs_subClassOf, nullptr) && + !lilv_world_ask(lworld, inst, owl_onDatatype, nullptr)) { + // ... which is not a subtype of another, add menu + add_class_menu_item(menu, inst, label); + } + } else { + // Value is not a class, add item + menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + std::string("_") + label, + sigc::bind(sigc::mem_fun(this, &URIEntry::uri_chosen), + std::string(lilv_node_as_uri(inst))))); + _app->set_tooltip(&menu->items().back(), inst); + } + } + + lilv_node_free(owl_onDatatype); + lilv_node_free(rdf_type); + lilv_node_free(rdfs_Class); + lilv_node_free(rdfs_Datatype); + lilv_node_free(rdfs_subClassOf); + + return menu; +} + +Gtk::Menu* +URIEntry::build_subclass_menu(const LilvNode* klass) +{ + World* world = _app->world(); + LilvWorld* lworld = world->lilv_world(); + + LilvNode* owl_onDatatype = lilv_new_uri(lworld, LILV_NS_OWL "onDatatype"); + LilvNode* rdfs_subClassOf = lilv_new_uri(lworld, LILV_NS_RDFS "subClassOf"); + + LilvNodes* subclasses = lilv_world_find_nodes( + lworld, nullptr, rdfs_subClassOf, klass); + LilvNodes* subtypes = lilv_world_find_nodes( + lworld, nullptr, owl_onDatatype, klass); + + if (lilv_nodes_size(subclasses) == 0 && lilv_nodes_size(subtypes) == 0) { + return nullptr; + } + + Gtk::Menu* menu = new Gtk::Menu(); + + // Add "header" item for choosing this class itself + add_leaf_menu_item(menu, klass, RDFS::label(world, klass)); + menu->items().push_back(Gtk::Menu_Helpers::SeparatorElem()); + + // Put subclasses/types in a map keyed by label (to sort menu) + std::map<std::string, const LilvNode*> entries; + LILV_FOREACH(nodes, s, subclasses) { + const LilvNode* node = lilv_nodes_get(subclasses, s); + entries.emplace(RDFS::label(world, node), node); + } + LILV_FOREACH(nodes, s, subtypes) { + const LilvNode* node = lilv_nodes_get(subtypes, s); + entries.emplace(RDFS::label(world, node), node); + } + + // Add an item (possibly with a submenu) for each subclass/type + for (const auto& e : entries) { + add_class_menu_item(menu, e.second, e.first); + } + + lilv_nodes_free(subtypes); + lilv_nodes_free(subclasses); + lilv_node_free(rdfs_subClassOf); + lilv_node_free(owl_onDatatype); + + return menu; +} + +void +URIEntry::add_leaf_menu_item(Gtk::Menu* menu, + const LilvNode* node, + const std::string& label) +{ + menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + std::string("_") + label, + sigc::bind(sigc::mem_fun(this, &URIEntry::uri_chosen), + std::string(lilv_node_as_uri(node))))); + + _app->set_tooltip(&menu->items().back(), node); +} + +void +URIEntry::add_class_menu_item(Gtk::Menu* menu, + const LilvNode* klass, + const std::string& label) +{ + Gtk::Menu* submenu = build_subclass_menu(klass); + + if (submenu) { + menu->items().push_back(Gtk::Menu_Helpers::MenuElem(label)); + menu->items().back().set_submenu(*Gtk::manage(submenu)); + } else { + add_leaf_menu_item(menu, klass, label); + } + + _app->set_tooltip(&menu->items().back(), klass); +} + +void +URIEntry::uri_chosen(const std::string& uri) +{ + _entry->set_text(uri); +} + +bool +URIEntry::menu_button_event(GdkEvent* ev) +{ + if (ev->type != GDK_BUTTON_PRESS) { + return false; + } + + Gtk::Menu* menu = Gtk::manage(build_value_menu()); + menu->popup(ev->button.button, ev->button.time); + + return true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/URIEntry.hpp b/src/gui/URIEntry.hpp new file mode 100644 index 00000000..2f55a3d9 --- /dev/null +++ b/src/gui/URIEntry.hpp @@ -0,0 +1,68 @@ +/* + This file is part of Ingen. + Copyright 2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_URI_ENTRY_HPP +#define INGEN_GUI_URI_ENTRY_HPP + +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/menu.h> + +#include "lilv/lilv.h" + +namespace Ingen { +namespace GUI { + +class App; + +class URIEntry : public Gtk::HBox { +public: + /** Create a widget for entering URIs. + * + * If `types` is given, then a menu button will be shown which pops up a + * enu for easily choosing known values with valid types. + */ + URIEntry(App* app, std::set<URI> types, const std::string& value); + + std::string get_text() { return _entry->get_text(); } + Glib::SignalProxy0<void> signal_changed() { return _entry->signal_changed(); } + +private: + Gtk::Menu* build_value_menu(); + Gtk::Menu* build_subclass_menu(const LilvNode* klass); + + void add_leaf_menu_item(Gtk::Menu* menu, + const LilvNode* node, + const std::string& label); + + void add_class_menu_item(Gtk::Menu* menu, + const LilvNode* klass, + const std::string& label); + + void uri_chosen(const std::string& uri); + bool menu_button_event(GdkEvent* ev); + + App* _app; + const std::set<URI> _types; + Gtk::Button* _menu_button; + Gtk::Entry* _entry; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_URI_ENTRY_HPP diff --git a/src/gui/WidgetFactory.cpp b/src/gui/WidgetFactory.cpp new file mode 100644 index 00000000..afb6a07f --- /dev/null +++ b/src/gui/WidgetFactory.cpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <fstream> +#include <string> + +#include "ingen/Log.hpp" +#include "ingen/runtime_paths.hpp" + +#include "WidgetFactory.hpp" + +namespace Ingen { +namespace GUI { + +Glib::ustring WidgetFactory::ui_filename = ""; + +inline static bool +is_readable(const std::string& filename) +{ + std::ifstream fs(filename.c_str()); + const bool fail = fs.fail(); + fs.close(); + return !fail; +} + +void +WidgetFactory::find_ui_file() +{ + // Try file in bundle (directory where executable resides) + ui_filename = Ingen::bundle_file_path("ingen_gui.ui"); + if (is_readable(ui_filename)) { + return; + } + + // Try ENGINE_UI_PATH from the environment + const char* const env_path = getenv("INGEN_UI_PATH"); + if (env_path && is_readable(env_path)) { + ui_filename = env_path; + return; + } + + // Try the default system installed path + ui_filename = Ingen::data_file_path("ingen_gui.ui"); + if (is_readable(ui_filename)) { + return; + } + + throw std::runtime_error((fmt("Unable to find ingen_gui.ui in %1%\n") + % INGEN_DATA_DIR).str()); +} + +Glib::RefPtr<Gtk::Builder> +WidgetFactory::create(const std::string& toplevel_widget) +{ + if (ui_filename.empty()) { + find_ui_file(); + } + + if (toplevel_widget.empty()) { + return Gtk::Builder::create_from_file(ui_filename); + } else { + return Gtk::Builder::create_from_file(ui_filename, toplevel_widget.c_str()); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/WidgetFactory.hpp b/src/gui/WidgetFactory.hpp new file mode 100644 index 00000000..92f4dffe --- /dev/null +++ b/src/gui/WidgetFactory.hpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GLADEFACTORY_HPP +#define INGEN_GUI_GLADEFACTORY_HPP + +#include <string> + +#include <glibmm.h> +#include <gtkmm/builder.h> + +namespace Ingen { +namespace GUI { + +/** Loads widgets from an XML description. + * Purely static. + * + * \ingroup GUI + */ +class WidgetFactory { +public: + static Glib::RefPtr<Gtk::Builder> + create(const std::string& toplevel_widget=""); + + template<typename T> + static void get_widget(const Glib::ustring& name, T*& widget) { + Glib::RefPtr<Gtk::Builder> xml = create(name); + xml->get_widget(name, widget); + } + + template<typename T> + static void get_widget_derived(const Glib::ustring& name, T*& widget) { + Glib::RefPtr<Gtk::Builder> xml = create(name); + xml->get_widget_derived(name, widget); + } + +private: + static void find_ui_file(); + static Glib::ustring ui_filename; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GLADEFACTORY_HPP diff --git a/src/gui/Window.hpp b/src/gui/Window.hpp new file mode 100644 index 00000000..2a5c9843 --- /dev/null +++ b/src/gui/Window.hpp @@ -0,0 +1,78 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_WINDOW_HPP +#define INGEN_GUI_WINDOW_HPP + +#include <gtkmm/dialog.h> +#include <gtkmm/window.h> + +namespace Ingen { + +namespace GUI { + +class App; + +/** Ingen GUI Window + * \ingroup GUI + */ +class Window : public Gtk::Window +{ +public: + Window() : Gtk::Window(), _app(nullptr) {} + explicit Window(BaseObjectType* cobject) : Gtk::Window(cobject), _app(nullptr) {} + + virtual void init_window(App& app) { _app = &app; } + + bool on_key_press_event(GdkEventKey* event) { + if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) { + hide(); + return true; + } + return Gtk::Window::on_key_press_event(event); + } + + static bool key_press_handler(Gtk::Window* win, GdkEventKey* event); + + App* _app; +}; + +/** Ingen GUI Dialog + * \ingroup GUI + */ +class Dialog : public Gtk::Dialog +{ +public: + Dialog() : Gtk::Dialog(), _app(nullptr) {} + explicit Dialog(BaseObjectType* cobject) : Gtk::Dialog(cobject), _app(nullptr) {} + + virtual void init_dialog(App& app) { _app = &app; } + + bool on_key_press_event(GdkEventKey* event) { + if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) { + hide(); + return true; + } + return Gtk::Window::on_key_press_event(event); + } + + App* _app; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_WINDOW_HPP diff --git a/src/gui/WindowFactory.cpp b/src/gui/WindowFactory.cpp new file mode 100644 index 00000000..5dbdbe98 --- /dev/null +++ b/src/gui/WindowFactory.cpp @@ -0,0 +1,302 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <stdexcept> +#include <string> + +#include "ingen/Log.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "LoadGraphWindow.hpp" +#include "LoadPluginWindow.hpp" +#include "NewSubgraphWindow.hpp" +#include "GraphView.hpp" +#include "GraphWindow.hpp" +#include "PropertiesWindow.hpp" +#include "RenameWindow.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +WindowFactory::WindowFactory(App& app) + : _app(app) + , _main_box(nullptr) + , _load_plugin_win(nullptr) + , _load_graph_win(nullptr) + , _new_subgraph_win(nullptr) + , _properties_win(nullptr) +{ + WidgetFactory::get_widget_derived("load_plugin_win", _load_plugin_win); + WidgetFactory::get_widget_derived("load_graph_win", _load_graph_win); + WidgetFactory::get_widget_derived("new_subgraph_win", _new_subgraph_win); + WidgetFactory::get_widget_derived("properties_win", _properties_win); + WidgetFactory::get_widget_derived("rename_win", _rename_win); + + if (!(_load_plugin_win && _load_graph_win && _new_subgraph_win + && _properties_win && _rename_win)) { + throw std::runtime_error("failed to load window widgets\n"); + } + + _load_plugin_win->init_window(app); + _load_graph_win->init(app); + _new_subgraph_win->init_window(app); + _properties_win->init_window(app); + _rename_win->init_window(app); +} + +WindowFactory::~WindowFactory() +{ + for (const auto& w : _graph_windows) { + delete w.second; + } +} + +void +WindowFactory::clear() +{ + for (const auto& w : _graph_windows) { + delete w.second; + } + + _graph_windows.clear(); +} + +/** Returns the number of Graph windows currently visible. + */ +size_t +WindowFactory::num_open_graph_windows() +{ + size_t ret = 0; + for (const auto& w : _graph_windows) { + if (w.second->is_visible()) { + ++ret; + } + } + + return ret; +} + +GraphBox* +WindowFactory::graph_box(SPtr<const GraphModel> graph) +{ + GraphWindow* window = graph_window(graph); + if (window) { + return window->box(); + } else { + return _main_box; + } +} + +GraphWindow* +WindowFactory::graph_window(SPtr<const GraphModel> graph) +{ + if (!graph) { + return nullptr; + } + + auto w = _graph_windows.find(graph->path()); + + return (w == _graph_windows.end()) ? nullptr : w->second; +} + +GraphWindow* +WindowFactory::parent_graph_window(SPtr<const BlockModel> block) +{ + if (!block) { + return nullptr; + } + + return graph_window(dynamic_ptr_cast<GraphModel>(block->parent())); +} + +/** Present a GraphWindow for a Graph. + * + * If `preferred` is not NULL, it will be set to display `graph` if the graph + * does not already have a visible window, otherwise that window will be + * presented and `preferred` left unmodified. + */ +void +WindowFactory::present_graph(SPtr<const GraphModel> graph, + GraphWindow* preferred, + SPtr<GraphView> view) +{ + assert(!view || view->graph() == graph); + + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + (*w).second->present(); + } else if (preferred) { + w = _graph_windows.find(preferred->graph()->path()); + assert((*w).second == preferred); + + preferred->box()->set_graph(graph, view); + _graph_windows.erase(w); + _graph_windows[graph->path()] = preferred; + preferred->present(); + + } else { + GraphWindow* win = new_graph_window(graph, view); + win->present(); + } +} + +GraphWindow* +WindowFactory::new_graph_window(SPtr<const GraphModel> graph, + SPtr<GraphView> view) +{ + assert(!view || view->graph() == graph); + + GraphWindow* win = nullptr; + WidgetFactory::get_widget_derived("graph_win", win); + if (!win) { + _app.log().error("Failed to load graph window widget\n"); + return nullptr; + } + + win->init_window(_app); + + win->box()->set_graph(graph, view); + _graph_windows[graph->path()] = win; + + win->signal_delete_event().connect( + sigc::bind<0>(sigc::mem_fun(this, &WindowFactory::remove_graph_window), + win)); + + return win; +} + +bool +WindowFactory::remove_graph_window(GraphWindow* win, GdkEventAny* ignored) +{ + if (_graph_windows.size() <= 1) { + return !_app.quit(win); + } + + auto w = _graph_windows.find(win->graph()->path()); + + assert((*w).second == win); + _graph_windows.erase(w); + + delete win; + + return false; +} + +void +WindowFactory::present_load_plugin(SPtr<const GraphModel> graph, + Properties data) +{ + _app.request_plugins_if_necessary(); + + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + _load_plugin_win->set_transient_for(*w->second); + } + + _load_plugin_win->set_modal(false); + _load_plugin_win->set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); + if (w->second) { + int width, height; + w->second->get_size(width, height); + _load_plugin_win->set_default_size(width - width / 8, height / 2); + } + _load_plugin_win->set_title( + std::string("Load Plugin - ") + graph->path() + " - Ingen"); + _load_plugin_win->present(graph, data); +} + +void +WindowFactory::present_load_graph(SPtr<const GraphModel> graph, + Properties data) +{ + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + _load_graph_win->set_transient_for(*w->second); + } + + _load_graph_win->present(graph, true, data); +} + +void +WindowFactory::present_load_subgraph(SPtr<const GraphModel> graph, + Properties data) +{ + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + _load_graph_win->set_transient_for(*w->second); + } + + _load_graph_win->present(graph, false, data); +} + +void +WindowFactory::present_new_subgraph(SPtr<const GraphModel> graph, + Properties data) +{ + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + _new_subgraph_win->set_transient_for(*w->second); + } + + _new_subgraph_win->present(graph, data); +} + +void +WindowFactory::present_rename(SPtr<const ObjectModel> object) +{ + auto w = _graph_windows.find(object->path()); + if (w == _graph_windows.end()) { + w = _graph_windows.find(object->path().parent()); + } + + if (w != _graph_windows.end()) { + _rename_win->set_transient_for(*w->second); + } + + _rename_win->present(object); +} + +void +WindowFactory::present_properties(SPtr<const ObjectModel> object) +{ + auto w = _graph_windows.find(object->path()); + if (w == _graph_windows.end()) { + w = _graph_windows.find(object->path().parent()); + } + if (w == _graph_windows.end()) { + w = _graph_windows.find(object->path().parent().parent()); + } + + if (w != _graph_windows.end()) { + _properties_win->set_transient_for(*w->second); + } + + _properties_win->present(object); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/WindowFactory.hpp b/src/gui/WindowFactory.hpp new file mode 100644 index 00000000..ea8b909b --- /dev/null +++ b/src/gui/WindowFactory.hpp @@ -0,0 +1,99 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_WINDOWFACTORY_HPP +#define INGEN_GUI_WINDOWFACTORY_HPP + +#include <map> + +#include "ingen/Node.hpp" +#include "ingen/types.hpp" + +namespace Ingen { + +namespace Client { +class BlockModel; +class ObjectModel; +class GraphModel; +} + +namespace GUI { + +class App; +class GraphBox; +class GraphView; +class GraphWindow; +class LoadGraphWindow; +class LoadPluginWindow; +class NewSubgraphWindow; +class PropertiesWindow; +class RenameWindow; + +/** Manager/Factory for all windows. + * + * This serves as a nice centralized spot for all window management issues, + * as well as an enumeration of all windows (the goal being to reduce that + * number as much as possible). + */ +class WindowFactory { +public: + explicit WindowFactory(App& app); + ~WindowFactory(); + + size_t num_open_graph_windows(); + + GraphBox* graph_box(SPtr<const Client::GraphModel> graph); + GraphWindow* graph_window(SPtr<const Client::GraphModel> graph); + GraphWindow* parent_graph_window(SPtr<const Client::BlockModel> block); + + void present_graph( + SPtr<const Client::GraphModel> graph, + GraphWindow* preferred = NULL, + SPtr<GraphView> view = SPtr<GraphView>()); + + void present_load_plugin(SPtr<const Client::GraphModel> graph, Properties data=Properties()); + void present_load_graph(SPtr<const Client::GraphModel> graph, Properties data=Properties()); + void present_load_subgraph(SPtr<const Client::GraphModel> graph, Properties data=Properties()); + void present_new_subgraph(SPtr<const Client::GraphModel> graph, Properties data=Properties()); + void present_rename(SPtr<const Client::ObjectModel> object); + void present_properties(SPtr<const Client::ObjectModel> object); + + bool remove_graph_window(GraphWindow* win, GdkEventAny* ignored = NULL); + + void set_main_box(GraphBox* box) { _main_box = box; } + + void clear(); + +private: + typedef std::map<Raul::Path, GraphWindow*> GraphWindowMap; + + GraphWindow* new_graph_window(SPtr<const Client::GraphModel> graph, + SPtr<GraphView> view); + + App& _app; + GraphBox* _main_box; + GraphWindowMap _graph_windows; + LoadPluginWindow* _load_plugin_win; + LoadGraphWindow* _load_graph_win; + NewSubgraphWindow* _new_subgraph_win; + PropertiesWindow* _properties_win; + RenameWindow* _rename_win; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_WINDOWFACTORY_HPP diff --git a/src/gui/ingen_gui.cpp b/src/gui/ingen_gui.cpp new file mode 100644 index 00000000..677296fd --- /dev/null +++ b/src/gui/ingen_gui.cpp @@ -0,0 +1,67 @@ +/* + This file is part of Ingen. + Copyright 2007-2018 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Configuration.hpp" +#include "ingen/Module.hpp" +#include "ingen/QueuedInterface.hpp" +#include "ingen/client/SigClientInterface.hpp" + +#include "App.hpp" + +namespace Ingen { +namespace GUI { + +struct GUIModule : public Module { + using SigClientInterface = Client::SigClientInterface; + + void load(World* world) { + URI uri(world->conf().option("connect").ptr<char>()); + if (!world->interface()) { + world->set_interface( + world->new_interface(URI(uri), make_client(world))); + } else if (!dynamic_ptr_cast<SigClientInterface>( + world->interface()->respondee())) { + world->interface()->set_respondee(make_client(world)); + } + + app = GUI::App::create(world); + } + + void run(World* world) { + app->run(); + } + + SPtr<Interface> make_client(World* const world) { + SPtr<SigClientInterface> sci(new SigClientInterface()); + return world->engine() ? sci : SPtr<Interface>(new QueuedInterface(sci)); + } + + SPtr<GUI::App> app; +}; + +} // namespace GUI +} // namespace Ingen + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + Glib::thread_init(); + return new Ingen::GUI::GUIModule(); +} + +} // extern "C" diff --git a/src/gui/ingen_gui.gladep b/src/gui/ingen_gui.gladep new file mode 100644 index 00000000..184ff460 --- /dev/null +++ b/src/gui/ingen_gui.gladep @@ -0,0 +1,9 @@ +<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*--> +<!DOCTYPE glade-project SYSTEM "http://glade.gnome.org/glade-project-2.0.dtd"> + +<glade-project> + <name>Ingen</name> + <program_name>ingen</program_name> + <language>C++</language> + <gnome_support>FALSE</gnome_support> +</glade-project> diff --git a/src/gui/ingen_gui.ui b/src/gui/ingen_gui.ui new file mode 100644 index 00000000..9e751064 --- /dev/null +++ b/src/gui/ingen_gui.ui @@ -0,0 +1,3049 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.24"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkAboutDialog" id="about_win"> + <property name="can_focus">False</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">normal</property> + <property name="program_name">Ingen</property> + <property name="version">@INGEN_VERSION@</property> + <property name="copyright" translatable="yes">Copyright 2005-2015 David Robillard <http://drobilla.net></property> + <property name="website">http://drobilla.net/software/ingen</property> + <property name="license" translatable="yes">Licensed under the GNU Affero GPL, Version 3 or later. + +See COPYING file included with this distribution, or http://www.gnu.org/licenses/agpl.txt for more information</property> + <property name="authors">David Robillard <d@drobilla.net></property> + <property name="translator_credits" translatable="yes" comments="TRANSLATORS: Replace this string with your names, one name per line.">translator-credits</property> + <property name="artists">Usability / UI Design: + Thorsten Wilms</property> + <property name="wrap_license">True</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox3"> + <property name="can_focus">False</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area3"> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkMenu" id="canvas_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckMenuItem" id="canvas_menu_edit"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Edit</property> + <property name="use_underline">True</property> + <property name="active">True</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="input1"> + <property name="label">_Input</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <child type="submenu"> + <object class="GtkMenu" id="input1_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_audio_input"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Audio</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_audio_input_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_cv_input"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">C_V</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_cv_input_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_control_input"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Control</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_control_input_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_event_input"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Event</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_event_input_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="output1"> + <property name="label">_Output</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <child type="submenu"> + <object class="GtkMenu" id="output1_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_audio_output"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Audio</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_audio_output_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_cv_output"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">C_V</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_cv_output_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_control_output"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Control</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_control_output_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_event_output"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Event</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_event_output_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="canvas_menu_load_plugin"> + <property name="label">_Find Plugin...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_canvas_menu_add_plugin_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="canvas_menu_load_graph"> + <property name="label">_Load Graph...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_canvas_menu_load_graph_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="canvas_menu_new_graph"> + <property name="label">_New Graph...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_canvas_menu_new_graph_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="canvas_menu_properties"> + <property name="label">P_roperties...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_canvas_menu_properties_activate" swapped="no"/> + </object> + </child> + </object> + <object class="GtkWindow" id="config_win"> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Configuration - Ingen</property> + <child> + <object class="GtkVBox" id="vbox13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkTable" id="table9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <child> + <object class="GtkLabel" id="label90"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Graph Search Path: </b></property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkEntry" id="config_path_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label91"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><i>Example: /foo/bar:/home/user/graphs</i></property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label103"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="config_save_button"> + <property name="label">gtk-save</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="config_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="config_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkDialog" id="connect_win"> + <property name="can_focus">False</property> + <property name="border_width">6</property> + <property name="title" translatable="yes">Engine - Ingen</property> + <property name="resizable">False</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="connect_quit_button"> + <property name="label">gtk-quit</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="connect_disconnect_button"> + <property name="label">gtk-disconnect</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="connect_connect_button"> + <property name="label">gtk-connect</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="hbox61"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="connect_icon"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xpad">12</property> + <property name="stock">gtk-disconnect</property> + <property name="icon-size">3</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkProgressBar" id="connect_progress_bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="pulse_step">0.10000000149</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="connect_progress_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Not connected</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">4</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table18"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">2</property> + <property name="row_spacing">8</property> + <child> + <object class="GtkHBox" id="hbox64"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkSpinButton" id="connect_port_spinbutton"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">GTK_FILL</property> + <property name="x_padding">8</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox67"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkEntry" id="connect_url_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="activates_default">True</property> + <property name="width_chars">28</property> + <property name="text" translatable="yes">unix:///tmp/ingen.sock</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + <property name="x_padding">8</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="connect_server_radiobutton"> + <property name="label" translatable="yes">_Connect to engine at: </property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="connect_launch_radiobutton"> + <property name="label" translatable="yes">_Launch separate engine on port: </property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">connect_server_radiobutton</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="connect_internal_radiobutton"> + <property name="label" translatable="yes">Start local _JACK engine</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">connect_server_radiobutton</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label131"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="connect_deactivate_button"> + <property name="label" translatable="yes">D_eactivate</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="connect_activate_button"> + <property name="label" translatable="yes">_Activate</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">6</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">connect_quit_button</action-widget> + <action-widget response="-6">connect_disconnect_button</action-widget> + <action-widget response="-6">connect_connect_button</action-widget> + </action-widgets> + </object> + <object class="GtkWindow" id="graph_tree_win"> + <property name="width_request">320</property> + <property name="height_request">340</property> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Graphs - Ingen</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow8"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">3</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="graphs_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="rules_hint">True</property> + </object> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="graph_win"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Ingen</property> + <property name="default_width">776</property> + <property name="default_height">480</property> + <child> + <object class="GtkVBox" id="graph_win_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuBar" id="menubar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuItem" id="graph_file_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_File</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="graph_file_menu_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="graph_import_menuitem"> + <property name="label">_Import...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="I" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_import_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_save_menuitem"> + <property name="label">gtk-save</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="S" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_file_save_graph_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_save_as_menuitem"> + <property name="label">Save _As...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="S" signal="activate" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_save_as_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_export_image_menuitem"> + <property name="label">_Export Image...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="R" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_draw_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_close_menuitem"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="W" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_file_close_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_quit_menuitem"> + <property name="label">gtk-quit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="Q" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_file_quit_nokill_menuitem_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="edit2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Edit</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="edit2_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="graph_undo_menuitem"> + <property name="label">gtk-undo</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="Z" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_undo_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_redo_menuitem"> + <property name="label">gtk-redo</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="Z" signal="activate" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_redo_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_cut_menuitem"> + <property name="label">gtk-cut</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_graph_cut_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_copy_menuitem"> + <property name="label">gtk-copy</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="C" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_copy_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_paste_menuitem"> + <property name="label">gtk-paste</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="V" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_paste_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_delete_menuitem"> + <property name="label">gtk-delete</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="Delete" signal="activate"/> + <signal name="activate" handler="on_graph_delete_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_select_all_menuitem"> + <property name="label">gtk-select-all</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="A" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_select_all_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_arrange_menuitem"> + <property name="label">Arrange</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="G" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_view_control_window_menuitem"> + <property name="label">C_ontrols...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="O" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_view_control_window_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_properties_menuitem"> + <property name="label">gtk-properties</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="P" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_properties_menuitem_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="graph_graph_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_View</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="graph_graph_menu_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckMenuItem" id="graph_animate_signals_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Update control ports as values change.</property> + <property name="label" translatable="yes">Animate Signa_ls</property> + <property name="use_underline">True</property> + <accelerator key="l" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_sprung_layout_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Sprung Layou_t</property> + <property name="use_underline">True</property> + <accelerator key="t" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_human_names_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Human names</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <accelerator key="H" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_show_port_names_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Port _Names</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <accelerator key="n" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_doc_pane_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Documentation Pane</property> + <property name="use_underline">True</property> + <accelerator key="D" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_status_bar_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Status Bar</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <accelerator key="b" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_zoom_in_menuitem"> + <property name="label">gtk-zoom-in</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="equal" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_zoom_out_menuitem"> + <property name="label">gtk-zoom-out</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="minus" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_zoom_normal_menuitem"> + <property name="label">gtk-zoom-100</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="0" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_zoom_full_menuitem"> + <property name="label">gtk-zoom-fit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="F" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="graph_increase_font_size_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Increase Font Size</property> + <property name="use_underline">True</property> + <accelerator key="Up" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="graph_decrease_font_size_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Decrease Font Size</property> + <property name="use_underline">True</property> + <accelerator key="Down" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="graph_normal_font_size_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Normal Font Size</property> + <property name="use_underline">True</property> + <accelerator key="1" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_parent_menuitem"> + <property name="label">_Parent</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="BackSpace" signal="activate"/> + <signal name="activate" handler="graph_parent_menuitem" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_refresh_menuitem"> + <property name="label">gtk-refresh</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="F5" signal="activate"/> + <signal name="activate" handler="graph_refresh_menuitem" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_fullscreen_menuitem"> + <property name="label">gtk-fullscreen</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="F11" signal="activate"/> + <signal name="activate" handler="graph_fullscreen_menuitem" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="view1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Windows</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_view1_activate" swapped="no"/> + <child type="submenu"> + <object class="GtkMenu" id="view1_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="graph_view_engine_window_menuitem"> + <property name="label">_Engine</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="E" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_view_engine_window_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_view_graph_tree_window_menuitem"> + <property name="label">_Graph Tree</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="T" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_view_tree_window_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_view_messages_window_menuitem"> + <property name="label">_Messages</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="M" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_view_messages_window_menuitem_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="help_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Help</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_help_menu_activate" swapped="no"/> + <child type="submenu"> + <object class="GtkMenu" id="help_menu_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="right-click_the_canvas_to_add_objects1"> + <property name="label">Right-click the canvas to add objects</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_help_about_menuitem"> + <property name="label">gtk-about</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_about1_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHPaned" id="graph_documentation_paned"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkAlignment" id="graph_win_alignment"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="graph_documentation_scrolledwindow"> + <property name="can_focus">False</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkStatusbar" id="graph_win_status_bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkFileChooserDialog" id="load_graph_win"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Load Graph - Ingen</property> + <property name="window_position">center-on-parent</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="vbox11"> + <property name="can_focus">False</property> + <property name="spacing">24</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="hbuttonbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="load_graph_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="load_graph_ok_button"> + <property name="label">gtk-open</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xscale">0</property> + <child> + <object class="GtkTable" id="table14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">3</property> + <property name="column_spacing">12</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkLabel" id="load_graph_poly_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Polyphony: </property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="load_graph_poly_from_file_radio"> + <property name="label" translatable="yes">Load from _File</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="has_tooltip">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">load_graph_poly_voices_radio</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="load_graph_ports_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Ports: </property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="load_graph_insert_ports_radio"> + <property name="label" translatable="yes">_Insert new ports</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="has_tooltip">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">load_graph_merge_ports_radio</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="load_graph_merge_ports_radio"> + <property name="label" translatable="yes">_Merge with existing ports</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="has_tooltip">True</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox58"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkRadioButton" id="load_graph_poly_voices_radio"> + <property name="label" translatable="yes">_Voices:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="has_tooltip">True</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="load_graph_poly_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="load_graph_symbol_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Symbol: </property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">load_graph_symbol_entry</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkEntry" id="load_graph_symbol_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">load_graph_cancel_button</action-widget> + <action-widget response="-5">load_graph_ok_button</action-widget> + </action-widgets> + </object> + <object class="GtkWindow" id="load_plugin_win"> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Load Plugin - Ingen</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <object class="GtkVBox" id="vbox9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">1</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">2</property> + <child> + <object class="GtkTreeView" id="load_plugin_plugins_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">2</property> + <property name="reorderable">True</property> + <property name="rules_hint">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">3</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkLabel" id="label66"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">Node _Symbol:</property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">load_plugin_name_entry</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox63"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkEntry" id="load_plugin_name_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="load_plugin_polyphonic_checkbutton"> + <property name="label" translatable="yes">_Polyphonic</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">8</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + <property name="x_padding">6</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="load_plugin_search_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="secondary_icon_stock">gtk-clear</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="load_plugin_filter_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <child> + <object class="GtkButton" id="load_plugin_close_button"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="load_plugin_add_button"> + <property name="label">gtk-add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="messages_win"> + <property name="width_request">400</property> + <property name="height_request">180</property> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Messages - Ingen</property> + <child> + <object class="GtkVBox" id="vbox12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTextView" id="messages_textview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="pixels_above_lines">1</property> + <property name="pixels_below_lines">1</property> + <property name="editable">False</property> + <property name="wrap_mode">word</property> + <property name="left_margin">5</property> + <property name="right_margin">5</property> + <property name="cursor_visible">False</property> + <property name="accepts_tab">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="messages_clear_button"> + <property name="label">gtk-clear</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="messages_close_button"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="new_subgraph_win"> + <property name="width_request">320</property> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Create Subgraph - Ingen</property> + <property name="resizable">False</property> + <property name="window_position">center-on-parent</property> + <property name="type_hint">dialog</property> + <child> + <object class="GtkVBox" id="vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Symbol: </property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">new_subgraph_name_entry</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_EXPAND</property> + <property name="x_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Polyphony: </property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">new_subgraph_polyphony_spinbutton</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_EXPAND</property> + <property name="x_padding">5</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="new_subgraph_polyphony_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + <property name="y_padding">4</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="new_subgraph_name_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + <property name="y_padding">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="new_subgraph_message_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="new_subgraph_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="new_subgraph_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkMenu" id="object_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkImageMenuItem" id="node_popup_gui_menuitem"> + <property name="label" translatable="yes">Show GUI...</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="node_embed_gui_menuitem"> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Embed GUI</property> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="node_enabled_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Enabled</property> + <property name="active">True</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="node_randomize_menuitem"> + <property name="label" translatable="yes">Randomi_ze</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="port_set_min_menuitem"> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Set Value as Mi_nimum</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="port_set_max_menuitem"> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Set Value as Ma_ximum</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="port_reset_range_menuitem"> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Re_set Range</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="port_expose_menuitem"> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Expose</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="object_menu_separator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="object_polyphonic_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">P_olyphonic</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="object_learn_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Learn</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="object_unlearn_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Unlearn</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="object_disconnect_menuitem"> + <property name="label">Dis_connect</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="object_destroy_menuitem"> + <property name="label">gtk-delete</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="object_rename_menuitem"> + <property name="label">_Rename...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="object_properties_menuitem"> + <property name="label">_Properties...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + </object> + <object class="GtkMenu" id="port_control_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="port_control_menu_properties"> + <property name="label">gtk-properties</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_port_control_menu_properties_activate" swapped="no"/> + </object> + </child> + </object> + <object class="GtkWindow" id="port_properties_win"> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Port Properties - Ingen</property> + <property name="resizable">False</property> + <property name="window_position">mouse</property> + <child> + <object class="GtkVBox" id="dialog-vbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">8</property> + <child> + <object class="GtkHButtonBox" id="dialog-action_area7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="port_properties_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="port_properties_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <property name="column_spacing">2</property> + <property name="row_spacing">4</property> + <child> + <object class="GtkSpinButton" id="port_properties_min_spinner"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + <property name="digits">5</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="port_properties_max_spinner"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + <property name="digits">5</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label138"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Minimum Value: </property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label139"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Maximum Value: </property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="properties_win"> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="window_position">center-on-parent</property> + <child> + <object class="GtkVBox" id="properties_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkScrolledWindow" id="properties_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkViewport" id="viewport2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkTable" id="properties_table"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_columns">3</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkComboBox" id="properties_key_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="properties_value_bin"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="properties_add_button"> + <property name="label">gtk-add</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="properties_buttonbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="properties_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="properties_apply_button"> + <property name="label">gtk-apply</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="properties_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="rename_win"> + <property name="width_request">250</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Rename</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <child> + <object class="GtkTable" id="table2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <property name="row_spacing">8</property> + <child> + <object class="GtkLabel" id="label95"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Symbol: </property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">rename_symbol_entry</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="rename_label_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Label: </property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">rename_label_entry</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="rename_symbol_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="invisible_char">●</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="rename_message_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">8</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="rename_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="rename_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="warehouse_win"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Warehouse - Ingen</property> + <child> + <object class="GtkTable" id="table8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">6</property> + <property name="column_spacing">12</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkVBox" id="toggle_control"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkLabel" id="toggle_control_name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">4</property> + <property name="label" translatable="yes"><b>Name</b></property> + <property name="use_markup">True</property> + <property name="single_line_mode">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="yalign">1</property> + <property name="yscale">0</property> + <property name="bottom_padding">1</property> + <property name="left_padding">1</property> + <property name="right_padding">4</property> + <child> + <object class="GtkHSeparator" id="hseparator7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="toggle_control_check"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="y_padding">8</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="control_panel_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkAlignment" id="alignment6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="yalign">0</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwin1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkVBox" id="control_panel_controls_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkVBox" id="graph_view_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="hbox70"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkToolbar" id="toolbar6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="toolbar_style">icons</property> + <property name="icon_size">1</property> + <child> + <object class="GtkToolItem" id="graph_view_breadcrumb_container"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToolbar" id="graph_view_toolbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="toolbar_style">icons</property> + <property name="show_arrow">False</property> + <property name="icon_size">1</property> + <child> + <object class="GtkToggleToolButton" id="graph_view_process_but"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="stock_id">gtk-execute</property> + <property name="active">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="toolitem7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="image1978"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xpad">4</property> + <property name="stock">gtk-copy</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="toolitem10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkSpinButton" id="graph_view_poly_spin"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + <property name="numeric">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="graph_view_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="events">GDK_EXPOSURE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_MOTION_MASK | GDK_BUTTON1_MOTION_MASK | GDK_BUTTON2_MOTION_MASK | GDK_BUTTON3_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_FOCUS_CHANGE_MASK | GDK_STRUCTURE_MASK | GDK_PROPERTY_CHANGE_MASK | GDK_VISIBILITY_NOTIFY_MASK | GDK_PROXIMITY_IN_MASK | GDK_PROXIMITY_OUT_MASK | GDK_SUBSTRUCTURE_MASK | GDK_SCROLL_MASK</property> + <property name="border_width">1</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="control_strip"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkLabel" id="control_strip_name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="yalign">1</property> + <property name="xpad">4</property> + <property name="label" translatable="yes"><b>Name</b></property> + <property name="use_markup">True</property> + <property name="single_line_mode">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="yalign">1</property> + <property name="yscale">0</property> + <property name="bottom_padding">1</property> + <property name="left_padding">1</property> + <property name="right_padding">4</property> + <child> + <object class="GtkHSeparator" id="hseparator6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="control_strip_spinner"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="width_chars">12</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="digits">4</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHScale" id="control_strip_slider"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="digits">63</property> + <property name="draw_value">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_padding">8</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="string_control"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkHBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkLabel" id="string_control_name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">4</property> + <property name="label" translatable="yes"><b>Name</b></property> + <property name="use_markup">True</property> + <property name="single_line_mode">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="string_control_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">●</property> + <property name="caps_lock_warning">False</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="y_padding">8</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/src/gui/ingen_gui_lv2.cpp b/src/gui/ingen_gui_lv2.cpp new file mode 100644 index 00000000..57881741 --- /dev/null +++ b/src/gui/ingen_gui_lv2.cpp @@ -0,0 +1,209 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/AtomReader.hpp" +#include "ingen/AtomSink.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/World.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/SigClientInterface.hpp" +#include "ingen/ingen.h" +#include "ingen/runtime_paths.hpp" +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/extensions/ui/ui.h" + +#include "App.hpp" +#include "GraphBox.hpp" + +#define INGEN_LV2_UI_URI INGEN_NS "GraphUIGtk2" + +namespace Ingen { + +/** A sink that writes atoms to a port via the UI extension. */ +struct IngenLV2AtomSink : public AtomSink { + IngenLV2AtomSink(URIs& uris, + LV2UI_Write_Function ui_write, + LV2UI_Controller ui_controller) + : _uris(uris) + , _ui_write(ui_write) + , _ui_controller(ui_controller) + {} + + bool write(const LV2_Atom* atom, int32_t default_id) { + _ui_write(_ui_controller, + 0, + lv2_atom_total_size(atom), + _uris.atom_eventTransfer, + atom); + return true; + } + + URIs& _uris; + LV2UI_Write_Function _ui_write; + LV2UI_Controller _ui_controller; +}; + +struct IngenLV2UI { + IngenLV2UI() + : argc(0) + , argv(nullptr) + , forge(nullptr) + , world(nullptr) + , sink(nullptr) + {} + + int argc; + char** argv; + Forge* forge; + World* world; + IngenLV2AtomSink* sink; + SPtr<GUI::App> app; + SPtr<GUI::GraphBox> view; + SPtr<Interface> engine; + SPtr<AtomReader> reader; + SPtr<Client::SigClientInterface> client; +}; + +} // namespace Ingen + +static LV2UI_Handle +instantiate(const LV2UI_Descriptor* descriptor, + const char* plugin_uri, + const char* bundle_path, + LV2UI_Write_Function write_function, + LV2UI_Controller controller, + LV2UI_Widget* widget, + const LV2_Feature* const* features) +{ +#if __cplusplus >= 201103L + using Ingen::SPtr; +#endif + + Ingen::set_bundle_path(bundle_path); + + Ingen::IngenLV2UI* ui = new Ingen::IngenLV2UI(); + + LV2_URID_Map* map = nullptr; + LV2_URID_Unmap* unmap = nullptr; + LV2_Log_Log* log = nullptr; + for (int i = 0; features[i]; ++i) { + if (!strcmp(features[i]->URI, LV2_URID__map)) { + map = (LV2_URID_Map*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_URID__unmap)) { + unmap = (LV2_URID_Unmap*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_LOG__log)) { + log = (LV2_Log_Log*)features[i]->data; + } + } + + ui->world = new Ingen::World(map, unmap, log); + ui->forge = new Ingen::Forge(ui->world->uri_map()); + + ui->world->load_configuration(ui->argc, ui->argv); + + if (!ui->world->load_module("client")) { + delete ui; + return nullptr; + } + + ui->sink = new Ingen::IngenLV2AtomSink( + ui->world->uris(), write_function, controller); + + // Set up an engine interface that writes LV2 atoms + ui->engine = SPtr<Ingen::Interface>( + new Ingen::AtomWriter( + ui->world->uri_map(), ui->world->uris(), *ui->sink)); + + ui->world->set_interface(ui->engine); + + // Create App and client + ui->app = Ingen::GUI::App::create(ui->world); + ui->client = SPtr<Ingen::Client::SigClientInterface>( + new Ingen::Client::SigClientInterface()); + ui->app->set_is_plugin(true); + ui->app->attach(ui->client); + + ui->reader = SPtr<Ingen::AtomReader>( + new Ingen::AtomReader(ui->world->uri_map(), + ui->world->uris(), + ui->world->log(), + *ui->client.get())); + + // Create empty root graph model + Ingen::Properties props; + props.emplace(ui->app->uris().rdf_type, + Ingen::Property(ui->app->uris().ingen_Graph)); + ui->app->store()->put(Ingen::main_uri(), props); + + // Create a GraphBox for the root and set as the UI widget + SPtr<const Ingen::Client::GraphModel> root = + Ingen::dynamic_ptr_cast<const Ingen::Client::GraphModel>( + ui->app->store()->object(Raul::Path("/"))); + ui->view = Ingen::GUI::GraphBox::create(*ui->app, root); + ui->view->unparent(); + *widget = ui->view->gobj(); + + // Request the actual root graph + ui->world->interface()->get(Ingen::main_uri()); + + return ui; +} + +static void +cleanup(LV2UI_Handle handle) +{ + Ingen::IngenLV2UI* ui = (Ingen::IngenLV2UI*)handle; + delete ui; +} + +static void +port_event(LV2UI_Handle handle, + uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void* buffer) +{ + Ingen::IngenLV2UI* ui = (Ingen::IngenLV2UI*)handle; + const LV2_Atom* atom = (const LV2_Atom*)buffer; + ui->reader->write(atom); +} + +static const void* +extension_data(const char* uri) +{ + return nullptr; +} + +static const LV2UI_Descriptor descriptor = { + INGEN_LV2_UI_URI, + instantiate, + cleanup, + port_event, + extension_data +}; + +LV2_SYMBOL_EXPORT +const LV2UI_Descriptor* +lv2ui_descriptor(uint32_t index) +{ + switch (index) { + case 0: + return &descriptor; + default: + return nullptr; + } +} diff --git a/src/gui/ingen_style.rc b/src/gui/ingen_style.rc new file mode 100644 index 00000000..4763e12a --- /dev/null +++ b/src/gui/ingen_style.rc @@ -0,0 +1,155 @@ +style "ingen-default" +{ + GtkMenuItem::selected_shadow_type = out + + GtkWidget::interior_focus = 1 + GtkWidget::focus_padding = 1 + + GtkButton::default_border = { 0, 0, 0, 0 } + GtkButton::default_outside_border = { 0, 0, 0, 0 } + + GtkCheckButton::indicator_size = 12 + GtkExpander::expander_size = 16 + GtkMenuBar::internal-padding = 0 + GtkPaned::handle_size = 6 + GtkRange::slider_width = 15 + GtkRange::stepper_size = 15 + GtkRange::trough_border = 0 + GtkScrollbar::min_slider_length = 30 + GtkTreeView::expander_size = 14 + GtkTreeView::odd_row_color = "#343" + + xthickness = 1 + ythickness = 1 + + fg[NORMAL] = "#B8BBB9" + fg[PRELIGHT] = "#B8BBB9" + fg[ACTIVE] = "#B8BBB9" + fg[SELECTED] = "#B8BBB9" + fg[INSENSITIVE] = "#48494B" + + bg[NORMAL] = "#1E2224" + bg[PRELIGHT] = "#333537" + bg[ACTIVE] = "#333537" + bg[SELECTED] = "#00A150" + bg[INSENSITIVE] = "#1E2224" + + base[NORMAL] = "#111" + base[PRELIGHT] = "#222" + base[ACTIVE] = "#0A2" + base[SELECTED] = "#0A2" + base[INSENSITIVE] = "#444" + + text[NORMAL] = "#FFF" + text[PRELIGHT] = "#FFF" + text[ACTIVE] = "#FFF" + text[SELECTED] = "#FFF" + text[INSENSITIVE] = "#666" + + engine "clearlooks" + { + contrast = 1.0 + } +} + +style "ingen-progressbar" = "ingen-default" +{ + xthickness = 1 + ythickness = 1 +} + +style "ingen-wide" = "ingen-default" +{ + xthickness = 2 + ythickness = 2 +} + +style "ingen-notebook" = "ingen-wide" +{ + bg[NORMAL] = "#383B39" + bg[ACTIVE] = "#383B39" +} + +style "ingen-tasklist" = "ingen-default" +{ + xthickness = 5 + ythickness = 3 +} + +style "ingen-menu" = "ingen-default" +{ + xthickness = 5 + ythickness = 5 + bg[NORMAL] = "#262626" +} + +style "ingen-menu-item" = "ingen-default" +{ + xthickness = 2 + ythickness = 3 +} + +style "ingen-menu-itembar" = "ingen-default" +{ + xthickness = 3 + ythickness = 3 +} + +style "ingen-tree" = "ingen-default" +{ + xthickness = 2 + ythickness = 2 +} + +style "ingen-frame-title" = "ingen-default" +{ + fg[NORMAL] = "#B8BBB9" +} + +style "ingen-panel" = "ingen-default" +{ + xthickness = 3 + ythickness = 3 +} + +style "ingen-tooltips" = "ingen-default" +{ + xthickness = 4 + ythickness = 4 + bg[NORMAL] = "#585B59" +} + +style "ingen-combo" = "ingen-default" +{ + xthickness = 1 + ythickness = 2 +} + +class "*Ingen*GtkWidget" style : highest "ingen-default" +class "*Ingen*GtkButton" style : highest "ingen-wide" +class "*Ingen*GtkRange" style : highest "ingen-wide" +class "*Ingen*GtkFrame" style : highest "ingen-wide" +class "*Ingen*GtkStatusbar" style : highest "ingen-wide" +class "*Ingen*GtkMenu" style : highest "ingen-menu" +class "*Ingen*GtkMenuItem" style : highest "ingen-menu-item" +widget_class "*Ingen*MenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkAccelMenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkRadioMenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkCheckMenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkImageMenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkSeparatorMenuItem.*" style : highest "ingen-menu-item" +class "*Ingen*GtkEntry" style : highest "ingen-wide" +widget_class "*Ingen*.tooltips.*.GtkToggleButton" style : highest "ingen-tasklist" +widget_class "*Ingen*.GtkTreeView.GtkButton" style : highest "ingen-tree" +widget_class "*Ingen*.GtkCTree.GtkButton" style : highest "ingen-tree" +widget_class "*Ingen*.GtkList.GtkButton" style : highest "ingen-tree" +widget_class "*Ingen*.GtkCList.GtkButton" style : highest "ingen-tree" +widget_class "*Ingen*.GtkFrame.GtkLabel" style : highest "ingen-frame-title" +widget_class "*Ingen*BasePWidget.GtkEventBox.GtkTable.GtkFrame" style : highest "ingen-panel" +widget "gtk-tooltips" style : highest "ingen-tooltips" +class "*Ingen*GtkNotebook" style : highest "ingen-notebook" +class "*Ingen*GtkProgressBar" style : highest "ingen-progressbar" +widget_class "*Ingen*.GtkComboBox.GtkButton" style : highest "ingen-combo" +widget_class "*Ingen*.GtkCombo.GtkButton" style : highest "ingen-combo" + +widget "*Ingen*" style : highest "ingen-default" diff --git a/src/gui/rgba.hpp b/src/gui/rgba.hpp new file mode 100644 index 00000000..dae3f179 --- /dev/null +++ b/src/gui/rgba.hpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_RGBA_HPP +#define INGEN_GUI_RGBA_HPP + +#include <cmath> + +namespace Ingen { +namespace GUI { + +static inline uint32_t +rgba_to_uint(uint8_t r, uint8_t g, uint8_t b, uint8_t a) +{ + return ((((uint32_t)(r)) << 24) | + (((uint32_t)(g)) << 16) | + (((uint32_t)(b)) << 8) | + (((uint32_t)(a)))); +} + +static inline uint8_t +mono_interpolate(uint8_t v1, uint8_t v2, float f) +{ + return ((int)rint((v2) * (f) + (v1) * (1 - (f)))); +} + +#define RGBA_R(x) (((uint32_t)(x)) >> 24) +#define RGBA_G(x) ((((uint32_t)(x)) >> 16) & 0xFF) +#define RGBA_B(x) ((((uint32_t)(x)) >> 8) & 0xFF) +#define RGBA_A(x) (((uint32_t)(x)) & 0xFF) + +static inline uint32_t +rgba_interpolate(uint32_t c1, uint32_t c2, float f) +{ + return rgba_to_uint( + mono_interpolate(RGBA_R(c1), RGBA_R(c2), f), + mono_interpolate(RGBA_G(c1), RGBA_G(c2), f), + mono_interpolate(RGBA_B(c1), RGBA_B(c2), f), + mono_interpolate(RGBA_A(c1), RGBA_A(c2), f)); +} + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_RGBA_HPP diff --git a/src/gui/wscript b/src/gui/wscript new file mode 100644 index 00000000..160afc03 --- /dev/null +++ b/src/gui/wscript @@ -0,0 +1,113 @@ +#!/usr/bin/env python +import waflib.extras.autowaf as autowaf +import waflib.Utils as Utils +import waflib.Options as Options + +def options(ctx): + opt = ctx.get_option_group('Configuration options') + opt.add_option('--light-theme', action='store_true', dest='light_theme', + help='use light coloured theme') + +def configure(conf): + autowaf.check_pkg(conf, 'glibmm-2.4', uselib_store='GLIBMM', + atleast_version='2.14.0', mandatory=False) + autowaf.check_pkg(conf, 'gthread-2.0', uselib_store='GTHREAD', + atleast_version='2.14.0', mandatory=False) + autowaf.check_pkg(conf, 'gtkmm-2.4', uselib_store='GTKMM', + atleast_version='2.12.0', mandatory=False) + autowaf.check_pkg(conf, 'gtkmm-2.4', uselib_store='NEW_GTKMM', + atleast_version='2.14.0', mandatory=False) + autowaf.check_pkg(conf, 'ganv-1', uselib_store='GANV', + atleast_version='1.5.4', mandatory=False) + if not Options.options.no_webkit: + autowaf.check_pkg(conf, 'webkit-1.0', uselib_store='WEBKIT', + atleast_version='1.4.0', mandatory=False) + + if conf.env.HAVE_GANV and conf.env.HAVE_GTKMM: + autowaf.define(conf, 'INGEN_BUILD_GUI', 1) + + if Options.options.light_theme: + autowaf.define(conf, 'INGEN_USE_LIGHT_THEME', 1) + +def build(bld): + obj = bld(features = 'cxx cxxshlib', + export_includes = ['../..'], + includes = ['../..'], + name = 'libingen_gui', + target = 'ingen_gui', + install_path = '${LIBDIR}', + use = 'libingen libingen_client') + autowaf.use_lib(bld, obj, ''' + GANV + GLADEMM + GLIBMM + GNOMECANVAS + GTKMM + LILV + LV2 + RAUL + SIGCPP + SORD + SOUP + SUIL + WEBKIT + ''') + + obj.source = ''' + App.cpp + Arc.cpp + BreadCrumbs.cpp + ConnectWindow.cpp + GraphBox.cpp + GraphCanvas.cpp + GraphPortModule.cpp + GraphTreeWindow.cpp + GraphView.cpp + GraphWindow.cpp + LoadGraphWindow.cpp + LoadPluginWindow.cpp + MessagesWindow.cpp + NewSubgraphWindow.cpp + NodeMenu.cpp + NodeModule.cpp + ObjectMenu.cpp + PluginMenu.cpp + Port.cpp + PortMenu.cpp + PropertiesWindow.cpp + RDFS.cpp + RenameWindow.cpp + Style.cpp + SubgraphModule.cpp + ThreadedLoader.cpp + URIEntry.cpp + WidgetFactory.cpp + WindowFactory.cpp + ingen_gui.cpp + ''' + + # XML UI definition + bld(features = 'subst', + source = 'ingen_gui.ui', + target = '../../ingen_gui.ui', + install_path = '${DATADIR}/ingen', + chmod = Utils.O755, + INGEN_VERSION = bld.env.INGEN_VERSION) + + # Gtk style + bld(features = 'subst', + is_copy = True, + source = 'ingen_style.rc', + target = '../../ingen_style.rc', + install_path = '${DATADIR}/ingen', + chmod = Utils.O755) + + # LV2 UI + obj = bld(features = 'cxx cxxshlib', + source = 'ingen_gui_lv2.cpp', + includes = ['.', '../..'], + name = 'ingen_gui_lv2', + target = 'ingen_gui_lv2', + install_path = '${LV2DIR}/ingen.lv2/', + use = 'libingen libingen_gui') + autowaf.use_lib(bld, obj, 'LV2 SERD SORD LILV RAUL GLIBMM GTKMM') diff --git a/src/ingen/ingen.cpp b/src/ingen/ingen.cpp new file mode 100644 index 00000000..d812d862 --- /dev/null +++ b/src/ingen/ingen.cpp @@ -0,0 +1,263 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <signal.h> + +#include <cstdlib> +#include <chrono> +#include <iostream> +#include <memory> +#include <string> + +#include "raul/Path.hpp" + +#include "ingen_config.h" + +#include "ingen/Configuration.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Parser.hpp" +#include "ingen/World.hpp" +#include "ingen/paths.hpp" +#include "ingen/runtime_paths.hpp" +#include "ingen/types.hpp" +#ifdef HAVE_SOCKET +#include "ingen/client/SocketClient.hpp" +#endif + +using namespace std; +using namespace Ingen; + +class DummyInterface : public Interface +{ + URI uri() const override { return URI("ingen:dummy"); } + void message(const Message& msg) override {} +}; + +unique_ptr<Ingen::World> world; + +static void +ingen_interrupt(int signal) +{ + if (signal == SIGTERM) { + cerr << "ingen: Terminated" << endl; + exit(EXIT_FAILURE); + } else { + cout << "ingen: Interrupted" << endl; + if (world && world->engine()) { + world->engine()->quit(); + } + } +} + +static void +ingen_try(bool cond, const char* msg) +{ + if (!cond) { + cerr << "ingen: error: " << msg << endl; + exit(EXIT_FAILURE); + } +} + +static int +print_version() +{ + cout << "ingen " << INGEN_VERSION + << " <http://drobilla.net/software/ingen>\n" + << "Copyright 2007-2017 David Robillard <http://drobilla.net>.\n" + << "License: <https://www.gnu.org/licenses/agpl-3.0>\n" + << "This is free software; you are free to change and redistribute it.\n" + << "There is NO WARRANTY, to the extent permitted by law." << endl; + return EXIT_SUCCESS; +} + +int +main(int argc, char** argv) +{ + Ingen::set_bundle_path_from_code((void*)&print_version); + + // Create world + try { + world = unique_ptr<Ingen::World>(new Ingen::World(nullptr, NULL, NULL)); + world->load_configuration(argc, argv); + if (argc <= 1) { + world->conf().print_usage("ingen", cout); + return EXIT_FAILURE; + } else if (world->conf().option("help").get<int32_t>()) { + world->conf().print_usage("ingen", cout); + return EXIT_SUCCESS; + } else if (world->conf().option("version").get<int32_t>()) { + return print_version(); + } + } catch (std::exception& e) { + cout << "ingen: error: " << e.what() << endl; + return EXIT_FAILURE; + } + + Configuration& conf = world->conf(); + if (conf.option("uuid").is_valid()) { + world->set_jack_uuid(conf.option("uuid").ptr<char>()); + } + + // Run engine + if (conf.option("engine").get<int32_t>()) { + if (world->conf().option("threads").get<int32_t>() < 1) { + cerr << "ingen: error: threads must be > 0" << endl; + return EXIT_FAILURE; + } + + ingen_try(world->load_module("server"), "Failed to load server module"); + + ingen_try(bool(world->engine()), "Unable to create engine"); + world->engine()->listen(); + } + +#ifdef HAVE_SOCKET + Client::SocketClient::register_factories(world.get()); +#endif + + // Load GUI if requested + if (conf.option("gui").get<int32_t>()) { + ingen_try(world->load_module("client"), "Failed to load client module"); + ingen_try(world->load_module("gui"), "Failed to load GUI module"); + } + + // If we don't have a local engine interface (from the GUI), use network + SPtr<Interface> engine_interface(world->interface()); + SPtr<Interface> dummy_client(new DummyInterface()); + if (!engine_interface) { + const char* const uri = conf.option("connect").ptr<char>(); + ingen_try(URI::is_valid(uri), + (fmt("Invalid URI <%1%>") % uri).str().c_str()); + engine_interface = world->new_interface(URI(uri), dummy_client); + + if (!engine_interface && !conf.option("gui").get<int32_t>()) { + cerr << (fmt("ingen: error: Failed to connect to `%1%'\n") % uri); + return EXIT_FAILURE; + } + + world->set_interface(engine_interface); + } + + // Activate the engine, if we have one + if (world->engine()) { + if (!world->load_module("jack") && !world->load_module("portaudio")) { + cerr << "ingen: error: Failed to load driver module" << endl; + return EXIT_FAILURE; + } + + if (!world->engine()->supports_dynamic_ports() && + !conf.option("load").is_valid()) { + cerr << "ingen: error: Initial graph required for driver" << endl; + return EXIT_FAILURE; + } + } + + // Load a graph + if (conf.option("load").is_valid()) { + boost::optional<Raul::Path> parent; + boost::optional<Raul::Symbol> symbol; + + const Atom& path_option = conf.option("path"); + if (path_option.is_valid()) { + if (Raul::Path::is_valid(path_option.ptr<char>())) { + const Raul::Path p(path_option.ptr<char>()); + if (!p.is_root()) { + parent = p.parent(); + symbol = Raul::Symbol(p.symbol()); + } + } else { + cerr << "Invalid path given: '" << path_option.ptr<char>() << endl; + } + } + + ingen_try(bool(world->parser()), "Failed to create parser"); + + const string graph = conf.option("load").ptr<char>(); + + engine_interface->get(URI("ingen:/plugins")); + engine_interface->get(main_uri()); + + std::lock_guard<std::mutex> lock(world->rdf_mutex()); + world->parser()->parse_file( + world.get(), engine_interface.get(), graph, parent, symbol); + } else if (conf.option("server-load").is_valid()) { + const char* path = conf.option("server-load").ptr<char>(); + if (serd_uri_string_has_scheme((const uint8_t*)path)) { + std::cout << "Loading " << path << " (server side)" << std::endl; + engine_interface->copy(URI(path), main_uri()); + } else { + SerdNode uri = serd_node_new_file_uri( + (const uint8_t*)path, nullptr, nullptr, true); + std::cout << "Loading " << (const char*)uri.buf + << " (server side)" << std::endl; + engine_interface->copy(URI((const char*)uri.buf), main_uri()); + serd_node_free(&uri); + } + } + + // Save the currently loaded graph + if (conf.option("save").is_valid()) { + const char* path = conf.option("save").ptr<char>(); + if (serd_uri_string_has_scheme((const uint8_t*)path)) { + std::cout << "Saving to " << path << std::endl; + engine_interface->copy(main_uri(), URI(path)); + } else { + SerdNode uri = serd_node_new_file_uri( + (const uint8_t*)path, nullptr, nullptr, true); + std::cout << "Saving to " << (const char*)uri.buf << std::endl; + engine_interface->copy(main_uri(), URI((const char*)uri.buf)); + serd_node_free(&uri); + } + } + + // Activate the engine now that the graph is loaded + if (world->engine()) { + world->engine()->flush_events(std::chrono::milliseconds(10)); + world->engine()->activate(); + } + + // Set up signal handlers that will set quit_flag on interrupt + signal(SIGINT, ingen_interrupt); + signal(SIGTERM, ingen_interrupt); + + if (conf.option("gui").get<int32_t>()) { + world->run_module("gui"); + } else if (world->engine()) { + // Run engine main loop until interrupt + while (world->engine()->main_iteration()) { + this_thread::sleep_for(chrono::milliseconds(125)); + } + } + + // Sleep for a half second to allow event queues to drain + this_thread::sleep_for(chrono::milliseconds(500)); + + // Shut down + if (world->engine()) { + world->engine()->deactivate(); + } + + // Save configuration to restore preferences on next run + const std::string path = conf.save( + world->uri_map(), "ingen", "options.ttl", Configuration::GLOBAL); + std::cout << (fmt("Saved configuration to %1%") % path) << std::endl; + + engine_interface.reset(); + + return 0; +} diff --git a/src/ingen/ingen.desktop b/src/ingen/ingen.desktop new file mode 100644 index 00000000..99192435 --- /dev/null +++ b/src/ingen/ingen.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Ingen +Comment=Create synthesizers and effects in a modular environment +Exec=ingen -eg +Terminal=false +Icon=ingen +Type=Application +Categories=Application;AudioVideo;Sound;Audio; diff --git a/src/ingen/ingen.grind b/src/ingen/ingen.grind new file mode 100644 index 00000000..9e60915b --- /dev/null +++ b/src/ingen/ingen.grind @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +export INGEN_MODULE_PATH="`pwd`/../../libs/engine/.libs:`pwd`/../../libs/gui/.libs:`pwd`/../../libs/client/.libs" +export INGEN_GLADE_PATH="`pwd`/../../libs/gui/ingen_gui.glade" +libtool --mode=execute valgrind ./ingen $@ diff --git a/src/runtime_paths.cpp b/src/runtime_paths.cpp new file mode 100644 index 00000000..8dbe5c0c --- /dev/null +++ b/src/runtime_paths.cpp @@ -0,0 +1,146 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <climits> +#include <cstdlib> +#include <cstdlib> +#include <sstream> +#include <string> + +#include <dlfcn.h> + +#include "ingen/runtime_paths.hpp" +#include "ingen/FilePath.hpp" + +#include "ingen_config.h" + +namespace Ingen { + +static FilePath bundle_path; + +#if defined(__APPLE__) +const char search_path_separator = ':'; +static const char* const library_prefix = "lib"; +static const char* const library_suffix = ".dylib"; +#elif defined(_WIN32) && !defined(__CYGWIN__) +const char search_path_separator = ';'; +static const char* const library_prefix = ""; +static const char* const library_suffix = ".dll"; +#else +const char search_path_separator = ':'; +static const char* const library_prefix = "lib"; +static const char* const library_suffix = ".so"; +#endif + +/** Must be called once at startup, and passed a pointer to a function + * that lives in the 'top level' of the bundle (e.g. the executable). + * Passing a function defined in a module etc. will not work! + */ +void +set_bundle_path_from_code(void* function) +{ + Dl_info dli; + dladdr(function, &dli); + +#ifdef BUNDLE + char bin_loc[PATH_MAX]; + realpath(dli.dli_fname, bin_loc); +#else + const char* bin_loc = dli.dli_fname; +#endif + + bundle_path = FilePath(bin_loc).parent_path(); +} + +void +set_bundle_path(const char* path) +{ + bundle_path = FilePath(path); +} + +/** Return the absolute path of a file in an Ingen LV2 bundle + */ +FilePath +bundle_file_path(const std::string& name) +{ + return bundle_path / name; +} + +/** Return the absolute path of a 'resource' file. + */ +FilePath +data_file_path(const std::string& name) +{ +#ifdef BUNDLE + return bundle_path / INGEN_DATA_DIR / name; +#else + return FilePath(INGEN_DATA_DIR) / name; +#endif +} + +/** Return the absolute path of a module (dynamically loaded shared library). + */ +FilePath +ingen_module_path(const std::string& name, FilePath dir) +{ + FilePath ret; + if (dir.empty()) { +#ifdef BUNDLE + dir = FilePath(bundle_path) / INGEN_MODULE_DIR; +#else + dir = FilePath(INGEN_MODULE_DIR); +#endif + } + + return dir / + (std::string(library_prefix) + "ingen_" + name + library_suffix); +} + +FilePath +user_config_dir() +{ + const char* const xdg_config_home = getenv("XDG_CONFIG_HOME"); + const char* const home = getenv("HOME"); + + if (xdg_config_home) { + return FilePath(xdg_config_home); + } else if (home) { + return FilePath(home) / ".config"; + } + + return FilePath(); +} + +std::vector<FilePath> +system_config_dirs() +{ + const char* const xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); + + std::vector<FilePath> paths; + if (xdg_config_dirs) { + std::istringstream ss(xdg_config_dirs); + std::string entry; + while (std::getline(ss, entry, search_path_separator)) { + paths.emplace_back(entry); + } + } else { + paths.emplace_back("/etc/xdg"); + } + + return paths; +} + +} // namespace Ingen diff --git a/src/server/ArcImpl.cpp b/src/server/ArcImpl.cpp new file mode 100644 index 00000000..5b96ca03 --- /dev/null +++ b/src/server/ArcImpl.cpp @@ -0,0 +1,114 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "ArcImpl.hpp" +#include "BlockImpl.hpp" +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "InputPort.hpp" +#include "OutputPort.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +/** Constructor for an arc from a block's output port. + * + * This handles both polyphonic and monophonic blocks, transparently to the + * user (InputPort). + */ +ArcImpl::ArcImpl(PortImpl* tail, PortImpl* head) + : _tail(tail) + , _head(head) +{ + assert(tail != head); + assert(tail->path() != head->path()); +} + +ArcImpl::~ArcImpl() +{ + if (is_linked()) { + InputPort* iport = dynamic_cast<InputPort*>(_head); + if (iport) { + iport->remove_arc(*this); + } + } +} + +const Raul::Path& +ArcImpl::tail_path() const +{ + return _tail->path(); +} + +const Raul::Path& +ArcImpl::head_path() const +{ + return _head->path(); +} + +BufferRef +ArcImpl::buffer(uint32_t voice, SampleCount offset) const +{ + return _tail->buffer(std::min(voice, _tail->poly() - 1)); +} + +bool +ArcImpl::must_mix() const +{ + return (_tail->poly() > _head->poly() || + (_tail->buffer(0)->is_sequence() != _head->buffer(0)->is_sequence())); +} + +bool +ArcImpl::can_connect(const PortImpl* src, const InputPort* dst) +{ + const Ingen::URIs& uris = src->bufs().uris(); + return ( + // (Audio | Control | CV) => (Audio | Control | CV) + ( (src->is_a(PortType::ID::CONTROL) || + src->is_a(PortType::ID::AUDIO) || + src->is_a(PortType::ID::CV)) + && (dst->is_a(PortType::ID::CONTROL) + || dst->is_a(PortType::ID::AUDIO) + || dst->is_a(PortType::ID::CV))) + + // Equal types + || (src->type() == dst->type() && + src->buffer_type() == dst->buffer_type()) + + // Control => atom:Float Value + || (src->is_a(PortType::ID::CONTROL) && dst->supports(uris.atom_Float)) + + // Audio => atom:Sound Value + || (src->is_a(PortType::ID::AUDIO) && dst->supports(uris.atom_Sound)) + + // atom:Float Value => Control + || (src->supports(uris.atom_Float) && dst->is_a(PortType::ID::CONTROL)) + + // atom:Float Value => CV + || (src->supports(uris.atom_Float) && dst->is_a(PortType::ID::CV)) + + // atom:Sound Value => Audio + || (src->supports(uris.atom_Sound) && dst->is_a(PortType::ID::AUDIO))); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/ArcImpl.hpp b/src/server/ArcImpl.hpp new file mode 100644 index 00000000..40a6d179 --- /dev/null +++ b/src/server/ArcImpl.hpp @@ -0,0 +1,84 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_ARC_IMPL_HPP +#define INGEN_ENGINE_ARC_IMPL_HPP + +#include <cstdlib> + +#include <boost/intrusive/slist.hpp> + +#include "ingen/Arc.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "raul/Deletable.hpp" + +#include "BufferRef.hpp" +#include "RunContext.hpp" + +namespace Ingen { +namespace Server { + +class PortImpl; +class InputPort; + +/** Represents a single inbound connection for an InputPort. + * + * This can be a group of ports (coming from a polyphonic Block) or + * a single Port. This class exists basically as an abstraction of mixing + * down polyphonic inputs, so InputPort can just deal with mixing down + * multiple connections (oblivious to the polyphonic situation of the + * connection itself). + * + * This is stored in an intrusive slist in InputPort. + * + * \ingroup engine + */ +class ArcImpl + : private Raul::Noncopyable + , public Arc + , public boost::intrusive::slist_base_hook<> +{ +public: + ArcImpl(PortImpl* tail, PortImpl* head); + ~ArcImpl(); + + inline PortImpl* tail() const { return _tail; } + inline PortImpl* head() const { return _head; } + + const Raul::Path& tail_path() const; + const Raul::Path& head_path() const; + + /** Get the buffer for a particular voice. + * An Arc is smart - it knows the destination port requesting the + * buffer, and will return accordingly (e.g. the same buffer for every + * voice in a mono->poly arc). + */ + BufferRef buffer(uint32_t voice, SampleCount offset=0) const; + + /** Whether this arc must mix down voices into a local buffer */ + bool must_mix() const; + + static bool can_connect(const PortImpl* src, const InputPort* dst); + +protected: + PortImpl* const _tail; + PortImpl* const _head; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_ARC_IMPL_HPP diff --git a/src/server/BlockFactory.cpp b/src/server/BlockFactory.cpp new file mode 100644 index 00000000..7dcfd6af --- /dev/null +++ b/src/server/BlockFactory.cpp @@ -0,0 +1,229 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> + +#include "lilv/lilv.h" + +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "ingen/World.hpp" +#include "internals/BlockDelay.hpp" +#include "internals/Controller.hpp" +#include "internals/Note.hpp" +#include "internals/Time.hpp" +#include "internals/Trigger.hpp" + +#include "BlockFactory.hpp" +#include "InternalPlugin.hpp" +#include "LV2Plugin.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +using namespace Internals; + +BlockFactory::BlockFactory(Ingen::World* world) + : _world(world) + , _has_loaded(false) +{ + load_internal_plugins(); +} + +BlockFactory::~BlockFactory() +{ + for (auto& p : _plugins) { + delete p.second; + } + + _plugins.clear(); +} + +const BlockFactory::Plugins& +BlockFactory::plugins() +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + if (!_has_loaded) { + load_lv2_plugins(); + _has_loaded = true; + } + return _plugins; +} + +std::set<PluginImpl*> +BlockFactory::refresh() +{ + // Record current plugins, and those that are currently zombies + const Plugins old_plugins(_plugins); + std::set<PluginImpl*> zombies; + for (const auto& p : _plugins) { + if (p.second->is_zombie()) { + zombies.insert(p.second); + } + } + + // Re-load plugins + load_lv2_plugins(); + + // Add any new plugins to response + std::set<PluginImpl*> new_plugins; + for (const auto& p : _plugins) { + auto o = old_plugins.find(p.first); + if (o == old_plugins.end()) { + new_plugins.insert(p.second); + } + } + + // Add any resurrected plugins to response + for (const auto& z : zombies) { + if (!z->is_zombie()) { + new_plugins.insert(z); + } + } + + return new_plugins; +} + +PluginImpl* +BlockFactory::plugin(const URI& uri) +{ + load_plugin(uri); + const Plugins::const_iterator i = _plugins.find(uri); + return ((i != _plugins.end()) ? i->second : nullptr); +} + +void +BlockFactory::load_internal_plugins() +{ + Ingen::URIs& uris = _world->uris(); + InternalPlugin* block_delay_plug = BlockDelayNode::internal_plugin(uris); + _plugins.emplace(block_delay_plug->uri(), block_delay_plug); + + InternalPlugin* controller_plug = ControllerNode::internal_plugin(uris); + _plugins.emplace(controller_plug->uri(), controller_plug); + + InternalPlugin* note_plug = NoteNode::internal_plugin(uris); + _plugins.emplace(note_plug->uri(), note_plug); + + InternalPlugin* time_plug = TimeNode::internal_plugin(uris); + _plugins.emplace(time_plug->uri(), time_plug); + + InternalPlugin* trigger_plug = TriggerNode::internal_plugin(uris); + _plugins.emplace(trigger_plug->uri(), trigger_plug); +} + +void +BlockFactory::load_plugin(const URI& uri) +{ + if (_has_loaded || _plugins.find(uri) != _plugins.end()) { + return; + } + + LilvNode* node = lilv_new_uri(_world->lilv_world(), uri.c_str()); + const LilvPlugins* plugs = lilv_world_get_all_plugins(_world->lilv_world()); + const LilvPlugin* plug = lilv_plugins_get_by_uri(plugs, node); + if (plug) { + LV2Plugin* const ingen_plugin = new LV2Plugin(_world, plug); + _plugins.emplace(uri, ingen_plugin); + } + lilv_node_free(node); +} + +/** Loads information about all LV2 plugins into internal plugin database. + */ +void +BlockFactory::load_lv2_plugins() +{ + // Build an array of port type nodes for checking compatibility + typedef std::vector< SPtr<LilvNode> > Types; + Types types; + for (unsigned t = PortType::ID::AUDIO; t <= PortType::ID::ATOM; ++t) { + const URI& uri(PortType((PortType::ID)t).uri()); + types.push_back( + SPtr<LilvNode>(lilv_new_uri(_world->lilv_world(), uri.c_str()), + lilv_node_free)); + } + + const LilvPlugins* plugins = lilv_world_get_all_plugins(_world->lilv_world()); + LILV_FOREACH(plugins, i, plugins) { + const LilvPlugin* lv2_plug = lilv_plugins_get(plugins, i); + const URI uri(lilv_node_as_uri(lilv_plugin_get_uri(lv2_plug))); + + // Ignore plugins that require features Ingen doesn't support + LilvNodes* features = lilv_plugin_get_required_features(lv2_plug); + bool supported = true; + LILV_FOREACH(nodes, f, features) { + const char* feature = lilv_node_as_uri(lilv_nodes_get(features, f)); + if (!_world->lv2_features().is_supported(feature)) { + supported = false; + _world->log().warn( + fmt("Ignoring <%1%>; required feature <%2%>\n") + % uri % feature); + break; + } + } + lilv_nodes_free(features); + if (!supported) { + continue; + } + + // Ignore plugins that are missing ports + if (!lilv_plugin_get_port_by_index(lv2_plug, 0)) { + _world->log().warn( + fmt("Ignoring <%1%>; missing or corrupt ports\n") % uri); + continue; + } + + const uint32_t n_ports = lilv_plugin_get_num_ports(lv2_plug); + for (uint32_t p = 0; p < n_ports; ++p) { + const LilvPort* port = lilv_plugin_get_port_by_index(lv2_plug, p); + supported = false; + for (const auto& t : types) { + if (lilv_port_is_a(lv2_plug, port, t.get())) { + supported = true; + break; + } + } + if (!supported && + !lilv_port_has_property(lv2_plug, + port, + _world->uris().lv2_connectionOptional)) { + _world->log().warn( + fmt("Ignoring <%1%>; unsupported port <%2%>\n") + % uri % lilv_node_as_string( + lilv_port_get_symbol(lv2_plug, port))); + break; + } + } + if (!supported) { + continue; + } + + auto p = _plugins.find(uri); + if (p == _plugins.end()) { + LV2Plugin* const plugin = new LV2Plugin(_world, lv2_plug); + _plugins.emplace(uri, plugin); + } else if (lilv_plugin_verify(lv2_plug)) { + p->second->set_is_zombie(false); + } + } + + _world->log().info(fmt("Loaded %1% plugins\n") % _plugins.size()); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/BlockFactory.hpp b/src/server/BlockFactory.hpp new file mode 100644 index 00000000..25885f75 --- /dev/null +++ b/src/server/BlockFactory.hpp @@ -0,0 +1,67 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BLOCKFACTORY_HPP +#define INGEN_ENGINE_BLOCKFACTORY_HPP + +#include <map> +#include <set> + +#include "ingen/World.hpp" +#include "ingen/types.hpp" +#include "raul/Noncopyable.hpp" + +namespace Ingen { +namespace Server { + +class PluginImpl; + +/** Discovers and loads plugin libraries. + * + * \ingroup engine + */ +class BlockFactory : public Raul::Noncopyable +{ +public: + explicit BlockFactory(Ingen::World* world); + ~BlockFactory(); + + /** Reload plugin list. + * + * @return The set of newly loaded plugins. + */ + std::set<PluginImpl*> refresh(); + + void load_plugin(const URI& uri); + + typedef std::map<URI, PluginImpl*> Plugins; + const Plugins& plugins(); + + PluginImpl* plugin(const URI& uri); + +private: + void load_lv2_plugins(); + void load_internal_plugins(); + + Plugins _plugins; + Ingen::World* _world; + bool _has_loaded; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BLOCKFACTORY_HPP diff --git a/src/server/BlockImpl.cpp b/src/server/BlockImpl.cpp new file mode 100644 index 00000000..e95645f9 --- /dev/null +++ b/src/server/BlockImpl.cpp @@ -0,0 +1,303 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cstdint> + +#include "raul/Array.hpp" + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "BlockImpl.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +BlockImpl::BlockImpl(PluginImpl* plugin, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : NodeImpl(plugin->uris(), parent, symbol) + , _plugin(plugin) + , _polyphony((polyphonic && parent) ? parent->internal_poly() : 1) + , _mark(Mark::UNVISITED) + , _polyphonic(polyphonic) + , _activated(false) + , _enabled(true) +{ + assert(_plugin); + assert(_polyphony > 0); +} + +BlockImpl::~BlockImpl() +{ + if (_activated) { + deactivate(); + } + + if (is_linked()) { + parent_graph()->remove_block(*this); + } +} + +Node* +BlockImpl::port(uint32_t index) const +{ + return (*_ports)[index]; +} + +const Resource* +BlockImpl::plugin() const +{ + return _plugin; +} + +const PluginImpl* +BlockImpl::plugin_impl() const +{ + return _plugin; +} + +void +BlockImpl::activate(BufferFactory& bufs) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + _activated = true; + for (uint32_t p = 0; p < num_ports(); ++p) { + PortImpl* const port = _ports->at(p); + port->activate(bufs); + } +} + +void +BlockImpl::deactivate() +{ + _activated = false; + for (uint32_t p = 0; p < num_ports(); ++p) { + PortImpl* const port = _ports->at(p); + port->deactivate(); + } +} + +bool +BlockImpl::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + if (!_polyphonic) { + poly = 1; + } + + if (_ports) { + for (uint32_t i = 0; i < _ports->size(); ++i) { + _ports->at(i)->prepare_poly(bufs, poly); + } + } + + return true; +} + +bool +BlockImpl::apply_poly(RunContext& context, uint32_t poly) +{ + if (!_polyphonic) { + poly = 1; + } + + _polyphony = poly; + + if (_ports) { + for (uint32_t i = 0; i < num_ports(); ++i) { + _ports->at(i)->apply_poly(context, poly); + } + } + + return true; +} + +void +BlockImpl::set_buffer_size(RunContext& context, + BufferFactory& bufs, + LV2_URID type, + uint32_t size) +{ + if (_ports) { + for (uint32_t i = 0; i < _ports->size(); ++i) { + PortImpl* const p = _ports->at(i); + if (p->buffer_type() == type) { + p->set_buffer_size(context, bufs, size); + } + } + } +} + +PortImpl* +BlockImpl::nth_port_by_type(uint32_t n, bool input, PortType type) +{ + uint32_t count = 0; + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + PortImpl* const port = _ports->at(i); + if (port->is_input() == input && port->type() == type) { + if (count++ == n) { + return port; + } + } + } + return nullptr; +} + +PortImpl* +BlockImpl::port_by_symbol(const char* symbol) +{ + for (uint32_t p = 0; _ports && p < _ports->size(); ++p) { + if (_ports->at(p)->symbol() == symbol) { + return _ports->at(p); + } + } + return nullptr; +} + +void +BlockImpl::pre_process(RunContext& context) +{ + // Mix down input ports + for (uint32_t i = 0; i < num_ports(); ++i) { + PortImpl* const port = _ports->at(i); + port->pre_process(context); + port->connect_buffers(); + } +} + +void +BlockImpl::bypass(RunContext& context) +{ + if (!_ports) { + return; + } + + // Prepare port buffers for reading, converting/mixing if necessary + for (uint32_t i = 0; i < _ports->size(); ++i) { + _ports->at(i)->connect_buffers(); + _ports->at(i)->pre_run(context); + } + + // Dumb bypass + for (PortType t : { PortType::AUDIO, PortType::CV, PortType::ATOM }) { + for (uint32_t i = 0;; ++i) { + PortImpl* in = nth_port_by_type(i, true, t); + PortImpl* out = nth_port_by_type(i, false, t); + if (!out) { + break; // Finished writing all outputs + } else if (in) { + // Copy corresponding input to output + for (uint32_t v = 0; v < _polyphony; ++v) { + out->buffer(v)->copy(context, in->buffer(v).get()); + } + } else { + // Output but no corresponding input, clear + for (uint32_t v = 0; v < _polyphony; ++v) { + out->buffer(v)->clear(); + } + } + } + } + post_process(context); +} + +void +BlockImpl::process(RunContext& context) +{ + pre_process(context); + + if (!_enabled) { + bypass(context); + post_process(context); + return; + } + + RunContext subcontext(context); + for (SampleCount offset = 0; offset < context.nframes();) { + // Find earliest offset of a value change + SampleCount chunk_end = context.nframes(); + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + PortImpl* const port = _ports->at(i); + if (port->type() == PortType::CONTROL && port->is_input()) { + const SampleCount o = port->next_value_offset( + offset, context.nframes()); + if (o < chunk_end) { + chunk_end = o; + } + } + } + + // Slice context into a chunk from now until the next change + subcontext.slice(offset, chunk_end - offset); + + // Prepare port buffers for reading, converting/mixing if necessary + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + _ports->at(i)->connect_buffers(offset); + _ports->at(i)->pre_run(subcontext); + } + + // Run the chunk + run(subcontext); + + // Emit control port outputs as events + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + PortImpl* const port = _ports->at(i); + if (port->type() == PortType::CONTROL && port->is_output()) { + // TODO: Only emit events when value has actually changed? + for (uint32_t v = 0; v < _polyphony; ++v) { + port->buffer(v)->append_event(offset, port->buffer(v)->value()); + } + } + } + + offset = chunk_end; + subcontext.slice(offset, chunk_end - offset); + } + + post_process(context); +} + +void +BlockImpl::post_process(RunContext& context) +{ + // Write output ports + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + _ports->at(i)->post_process(context); + } +} + +void +BlockImpl::set_port_buffer(uint32_t voice, + uint32_t port_num, + BufferRef buf, + SampleCount offset) +{ + /*std::cout << path() << " set port " << port_num << " voice " << voice + << " buffer " << buf << " offset " << offset << std::endl;*/ +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/BlockImpl.hpp b/src/server/BlockImpl.hpp new file mode 100644 index 00000000..d663e319 --- /dev/null +++ b/src/server/BlockImpl.hpp @@ -0,0 +1,207 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BLOCKIMPL_HPP +#define INGEN_ENGINE_BLOCKIMPL_HPP + +#include <set> + +#include <boost/intrusive/slist.hpp> +#include <boost/optional.hpp> + +#include "lilv/lilv.h" + +#include "raul/Array.hpp" + +#include "BufferRef.hpp" +#include "NodeImpl.hpp" +#include "PluginImpl.hpp" +#include "PortType.hpp" +#include "RunContext.hpp" +#include "types.hpp" + +namespace Raul { +class Maid; +} + +namespace Ingen { +namespace Server { + +class Buffer; +class BufferFactory; +class Engine; +class GraphImpl; +class PluginImpl; +class PortImpl; +class RunContext; +class Worker; + +/** A Block in a Graph (which is also a Block). + * + * This is what is often called a "Module" in modular synthesizers. A Block is + * a unit with input/output ports, a process() method, and some other things. + * + * \ingroup engine + */ +class BlockImpl : public NodeImpl + , public boost::intrusive::slist_base_hook<> // In GraphImpl +{ +public: + typedef Raul::Array<PortImpl*> Ports; + + BlockImpl(PluginImpl* plugin, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate rate); + + virtual ~BlockImpl(); + + virtual GraphType graph_type() const { return GraphType::BLOCK; } + + /** Activate this Block. + * + * This function must be called in a non-realtime thread before it is + * inserted in to a graph. Any non-realtime actions that need to be + * done before the Block is ready for use should be done here. + */ + virtual void activate(BufferFactory& bufs); + + /** Deactivate this Block. + * + * This function must be called in a non-realtime thread after the + * block has been removed from its graph (i.e. processing is finished). + */ + virtual void deactivate(); + + /** Duplicate this Node. */ + virtual BlockImpl* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) { return nullptr; } + + /** Return true iff this block is activated */ + bool activated() const { return _activated; } + + /** Return true iff this block is enabled (not bypassed). */ + bool enabled() const { return _enabled; } + + /** Enable or disable (bypass) this block. */ + void set_enabled(bool e) { _enabled = e; } + + /** Load a preset from the world for this block. */ + virtual LilvState* load_preset(const URI& uri) { return nullptr; } + + /** Restore `state`. */ + virtual void apply_state(const UPtr<Worker>& worker, const LilvState* state) {} + + /** Save current state as preset. */ + virtual boost::optional<Resource> + save_preset(const URI& bundle, + const Properties& props) { return boost::optional<Resource>(); } + + /** Learn the next incoming MIDI event (for internals) */ + virtual void learn() {} + + /** Do whatever needs doing in the process thread before process() is called */ + virtual void pre_process(RunContext& context); + + /** Run block for an entire process cycle (calls run()). */ + virtual void process(RunContext& context); + + /** Bypass block for an entire process cycle (called from process()). */ + virtual void bypass(RunContext& context); + + /** Run block for a portion of process cycle (called from process()). */ + virtual void run(RunContext& context) = 0; + + /** Do whatever needs doing in the process thread after process() is called */ + virtual void post_process(RunContext& context); + + /** Set the buffer of a port to a given buffer (e.g. connect plugin to buffer) */ + virtual void set_port_buffer(uint32_t voice, + uint32_t port_num, + BufferRef buf, + SampleCount offset); + + virtual Node* port(uint32_t index) const; + virtual PortImpl* port_impl(uint32_t index) const { return (*_ports)[index]; } + + /** Get a port by symbol. */ + virtual PortImpl* port_by_symbol(const char* symbol); + + /** Blocks that are connected to this Block's inputs. */ + std::set<BlockImpl*>& providers() { return _providers; } + + /** Blocks that are connected to this Block's outputs. */ + std::set<BlockImpl*>& dependants() { return _dependants; } + + /** Flag block as polyphonic. + * + * Note this will not actually allocate voices etc., prepare_poly + * and apply_poly must be called after this function to truly make + * a block polyphonic. + */ + virtual void set_polyphonic(bool p) { _polyphonic = p; } + + virtual bool prepare_poly(BufferFactory& bufs, uint32_t poly); + virtual bool apply_poly(RunContext& context, uint32_t poly); + + /** Information about the Plugin this Block is an instance of. + * Not the best name - not all blocks come from plugins (ie Graph) + */ + virtual const Resource* plugin() const; + + /** Information about the Plugin this Block is an instance of. + * Not the best name - not all blocks come from plugins (ie Graph) + */ + virtual const PluginImpl* plugin_impl() const; + + virtual void plugin(PluginImpl* pi) { _plugin = pi; } + + virtual void set_buffer_size(RunContext& context, + BufferFactory& bufs, + LV2_URID type, + uint32_t size); + + /** The Graph this Block belongs to. */ + inline GraphImpl* parent_graph() const { return (GraphImpl*)_parent; } + + uint32_t num_ports() const { return _ports ? _ports->size() : 0; } + virtual uint32_t polyphony() const { return _polyphony; } + + /** Mark used during graph compilation */ + enum class Mark { UNVISITED, VISITING, VISITED }; + Mark get_mark() const { return _mark; } + void set_mark(Mark m) { _mark = m; } + +protected: + PortImpl* nth_port_by_type(uint32_t n, bool input, PortType type); + + PluginImpl* _plugin; + MPtr<Ports> _ports; ///< Access in audio thread only + uint32_t _polyphony; + std::set<BlockImpl*> _providers; ///< Blocks connected to this one's input ports + std::set<BlockImpl*> _dependants; ///< Blocks this one's output ports are connected to + Mark _mark; ///< Mark for graph compilation algorithm + bool _polyphonic; + bool _activated; + bool _enabled; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BLOCKIMPL_HPP diff --git a/src/server/Broadcaster.cpp b/src/server/Broadcaster.cpp new file mode 100644 index 00000000..00fefddd --- /dev/null +++ b/src/server/Broadcaster.cpp @@ -0,0 +1,97 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <utility> + +#include "ingen/Interface.hpp" + +#include "Broadcaster.hpp" +#include "PluginImpl.hpp" +#include "BlockFactory.hpp" + +namespace Ingen { +namespace Server { + +Broadcaster::Broadcaster() + : _must_broadcast(false) + , _bundle_depth(0) +{} + +Broadcaster::~Broadcaster() +{ + std::lock_guard<std::mutex> lock(_clients_mutex); + _clients.clear(); + _broadcastees.clear(); +} + +/** Register a client to receive messages over the notification band. + */ +void +Broadcaster::register_client(SPtr<Interface> client) +{ + std::lock_guard<std::mutex> lock(_clients_mutex); + _clients.insert(client); +} + +/** Remove a client from the list of registered clients. + * + * @return true if client was found and removed. + */ +bool +Broadcaster::unregister_client(SPtr<Interface> client) +{ + std::lock_guard<std::mutex> lock(_clients_mutex); + const size_t erased = _clients.erase(client); + _broadcastees.erase(client); + return (erased > 0); +} + +void +Broadcaster::set_broadcast(SPtr<Interface> client, bool broadcast) +{ + if (broadcast) { + _broadcastees.insert(client); + } else { + _broadcastees.erase(client); + } + _must_broadcast.store(!_broadcastees.empty()); +} + +void +Broadcaster::send_plugins(const BlockFactory::Plugins& plugins) +{ + std::lock_guard<std::mutex> lock(_clients_mutex); + for (const auto& c : _clients) { + send_plugins_to(c.get(), plugins); + } +} + +void +Broadcaster::send_plugins_to(Interface* client, + const BlockFactory::Plugins& plugins) +{ + client->bundle_begin(); + + for (const auto& p : plugins) { + const PluginImpl* const plugin = p.second; + client->put(plugin->uri(), plugin->properties()); + } + + client->bundle_end(); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Broadcaster.hpp b/src/server/Broadcaster.hpp new file mode 100644 index 00000000..3981b265 --- /dev/null +++ b/src/server/Broadcaster.hpp @@ -0,0 +1,118 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BROADCASTER_HPP +#define INGEN_ENGINE_BROADCASTER_HPP + +#include <atomic> +#include <list> +#include <mutex> +#include <set> +#include <string> + +#include "ingen/Interface.hpp" +#include "ingen/types.hpp" + +#include "BlockFactory.hpp" + +namespace Ingen { +namespace Server { + +/** Broadcaster for all clients. + * + * This is an Interface that forwards all messages to all registered + * clients (for updating all clients on state changes in the engine). + * + * \ingroup engine + */ +class Broadcaster : public Interface +{ +public: + Broadcaster(); + ~Broadcaster(); + + void register_client(SPtr<Interface> client); + bool unregister_client(SPtr<Interface> client); + + void set_broadcast(SPtr<Interface> client, bool broadcast); + + /** Ignore a client when broadcasting. + * + * This is used to prevent feeding back updates to the client that + * initiated a property set in the first place. + */ + void set_ignore_client(SPtr<Interface> client) { _ignore_client = client; } + void clear_ignore_client() { _ignore_client.reset(); } + + /** Return true iff there are any clients with broadcasting enabled. + * + * This is used in the audio thread to decide whether or not notifications + * should be calculated and emitted. + */ + bool must_broadcast() const { return _must_broadcast; } + + /** A handle that represents a transfer of possibly several changes. + * + * This object going out of scope signifies the transfer is completed. + * This makes doing the right thing in recursive functions that send + * updates simple (e.g. Event::post_process()). + */ + class Transfer : public Raul::Noncopyable { + public: + explicit Transfer(Broadcaster& b) : broadcaster(b) { + if (++broadcaster._bundle_depth == 1) { + broadcaster.bundle_begin(); + } + } + ~Transfer() { + if (--broadcaster._bundle_depth == 0) { + broadcaster.bundle_end(); + } + } + Broadcaster& broadcaster; + }; + + void send_plugins(const BlockFactory::Plugins& plugins); + void send_plugins_to(Interface*, const BlockFactory::Plugins& plugins); + + void message(const Message& msg) override { + std::lock_guard<std::mutex> lock(_clients_mutex); + for (const auto& c : _clients) { + if (c != _ignore_client) { + c->message(msg); + } + } + } + + URI uri() const override { return URI("ingen:/broadcaster"); } + +private: + friend class Transfer; + + typedef std::set<SPtr<Interface>> Clients; + + std::mutex _clients_mutex; + Clients _clients; + std::set< SPtr<Interface> > _broadcastees; + std::atomic<bool> _must_broadcast; + unsigned _bundle_depth; + SPtr<Interface> _ignore_client; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BROADCASTER_HPP diff --git a/src/server/Buffer.cpp b/src/server/Buffer.cpp new file mode 100644 index 00000000..34867fa3 --- /dev/null +++ b/src/server/Buffer.cpp @@ -0,0 +1,468 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#define __STDC_LIMIT_MACROS 1 + +#include <cmath> +#include <cstdint> +#include <cstring> +#include <new> + +#ifdef __SSE__ +# include <xmmintrin.h> +#endif + +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen_config.h" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "ingen/Log.hpp" + +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "RunContext.hpp" + +namespace Ingen { +namespace Server { + +Buffer::Buffer(BufferFactory& bufs, + LV2_URID type, + LV2_URID value_type, + uint32_t capacity, + bool external, + void* buf) + : _factory(bufs) + , _next(nullptr) + , _buf(external ? nullptr : aligned_alloc(capacity)) + , _latest_event(0) + , _type(type) + , _value_type(value_type) + , _capacity(capacity) + , _refs(0) + , _external(external) +{ + if (!external && !_buf) { + bufs.engine().log().rt_error("Failed to allocate buffer\n"); + throw std::bad_alloc(); + } + + if (type != bufs.uris().atom_Sound) { + /* Audio buffers are not atoms, the buffer is the start of a float + array which is already silent since the buffer is zeroed. All other + buffers are atoms. */ + if (_buf) { + LV2_Atom* atom = get<LV2_Atom>(); + atom->size = capacity - sizeof(LV2_Atom); + atom->type = type; + + clear(); + } + + if (value_type && value_type != type) { + /* Buffer with a different value type. These buffers (probably + sequences) have a "value" that persists independently of the buffer + contents. This is used to represent things like a Sequence of + Float, which acts like an individual float (has a value), but the + buffer itself only transmits changes and does not necessarily + contain the current value. */ + _value_buffer = bufs.get_buffer(value_type, 0, 0); + } + } +} + +Buffer::~Buffer() +{ + if (!_external) { + free(_buf); + } +} + +void +Buffer::recycle() +{ + _factory.recycle(this); +} + +void +Buffer::set_type(GetFn get, LV2_URID type, LV2_URID value_type) +{ + _type = type; + _value_type = value_type; + if (type == _factory.uris().atom_Sequence && value_type) { + _value_buffer = (_factory.*get)(value_type, 0, 0); + } +} + +void +Buffer::clear() +{ + if (is_audio() && _buf) { + memset(_buf, 0, _capacity); + } else if (is_control()) { + get<LV2_Atom_Float>()->body = 0; + } else if (is_sequence()) { + LV2_Atom_Sequence* seq = get<LV2_Atom_Sequence>(); + seq->atom.type = _factory.uris().atom_Sequence; + seq->atom.size = sizeof(LV2_Atom_Sequence_Body); + seq->body.unit = 0; + seq->body.pad = 0; + _latest_event = 0; + } +} + +void +Buffer::render_sequence(const RunContext& context, const Buffer* src, bool add) +{ + const LV2_URID atom_Float = _factory.uris().atom_Float; + const LV2_Atom_Sequence* seq = src->get<const LV2_Atom_Sequence>(); + const LV2_Atom_Float* init = (const LV2_Atom_Float*)src->value(); + float value = init ? init->body : 0.0f; + SampleCount offset = context.offset(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->time.frames >= offset && ev->body.type == atom_Float) { + write_block(value, offset, ev->time.frames, add); + value = ((const LV2_Atom_Float*)&ev->body)->body; + offset = ev->time.frames; + } + } + write_block(value, offset, context.offset() + context.nframes(), add); +} + +void +Buffer::copy(const RunContext& context, const Buffer* src) +{ + if (!_buf) { + return; + } else if (_type == src->type()) { + const uint32_t src_size = src->size(); + if (src_size <= _capacity) { + memcpy(_buf, src->_buf, src_size); + } else { + clear(); + } + } else if (src->is_audio() && is_control()) { + samples()[0] = src->samples()[0]; + } else if (src->is_control() && is_audio()) { + set_block(src->samples()[0], 0, context.nframes()); + } else if (src->is_sequence() && is_audio() && + src->value_type() == _factory.uris().atom_Float) { + render_sequence(context, src, false); + } else { + clear(); + } +} + +void +Buffer::resize(uint32_t capacity) +{ + if (!_external) { + _buf = realloc(_buf, capacity); + _capacity = capacity; + clear(); + } else { + _factory.engine().log().error("Attempt to resize external buffer\n"); + } +} + +void* +Buffer::port_data(PortType port_type, SampleCount offset) +{ + switch (port_type.id()) { + case PortType::ID::CONTROL: + return &_value_buffer->get<LV2_Atom_Float>()->body; + case PortType::ID::CV: + case PortType::ID::AUDIO: + if (_type == _factory.uris().atom_Float) { + return &get<LV2_Atom_Float>()->body; + } else if (_type == _factory.uris().atom_Sound) { + return (Sample*)_buf + offset; + } + break; + case PortType::ID::ATOM: + if (_type != _factory.uris().atom_Sound) { + return _buf; + } + default: break; + } + return nullptr; +} + +const void* +Buffer::port_data(PortType port_type, SampleCount offset) const +{ + return const_cast<void*>( + const_cast<Buffer*>(this)->port_data(port_type, offset)); +} + +#ifdef __SSE__ +/** Vector fabsf */ +static inline __m128 +mm_abs_ps(__m128 x) +{ + const __m128 sign_mask = _mm_set1_ps(-0.0f); // -0.0f = 1 << 31 + return _mm_andnot_ps(sign_mask, x); +} +#endif + +float +Buffer::peak(const RunContext& context) const +{ +#ifdef __SSE__ + const __m128* const vbuf = (const __m128*)samples(); + __m128 vpeak = mm_abs_ps(vbuf[0]); + const SampleCount nblocks = context.nframes() / 4; + + // First, find the vector absolute max of the buffer + for (SampleCount i = 1; i < nblocks; ++i) { + vpeak = _mm_max_ps(vpeak, mm_abs_ps(vbuf[i])); + } + + // Now we need the single max of vpeak + // vpeak = ABCD + // tmp = CDAB + __m128 tmp = _mm_shuffle_ps(vpeak, vpeak, _MM_SHUFFLE(2, 3, 0, 1)); + + // vpeak = MAX(A,C) MAX(B,D) MAX(C,A) MAX(D,B) + vpeak = _mm_max_ps(vpeak, tmp); + + // tmp = BADC of the new vpeak + // tmp = MAX(B,D) MAX(A,C) MAX(D,B) MAX(C,A) + tmp = _mm_shuffle_ps(vpeak, vpeak, _MM_SHUFFLE(1, 0, 3, 2)); + + // vpeak = MAX(MAX(A,C), MAX(B,D)), ... + vpeak = _mm_max_ps(vpeak, tmp); + + // peak = vpeak[0] + float peak; + _mm_store_ss(&peak, vpeak); + + return peak; +#else + const Sample* const buf = samples(); + float peak = 0.0f; + for (SampleCount i = 0; i < context.nframes(); ++i) { + peak = fmaxf(peak, fabsf(buf[i])); + } + return peak; +#endif +} + +void +Buffer::prepare_write(RunContext& context) +{ + if (_type == _factory.uris().atom_Sequence) { + LV2_Atom* atom = get<LV2_Atom>(); + + atom->type = (LV2_URID)_factory.uris().atom_Sequence; + atom->size = sizeof(LV2_Atom_Sequence_Body); + _latest_event = 0; + } +} + +void +Buffer::prepare_output_write(RunContext& context) +{ + if (_type == _factory.uris().atom_Sequence) { + LV2_Atom* atom = get<LV2_Atom>(); + + atom->type = (LV2_URID)_factory.uris().atom_Chunk; + atom->size = _capacity - sizeof(LV2_Atom); + _latest_event = 0; + } +} + +bool +Buffer::append_event(int64_t frames, + uint32_t size, + uint32_t type, + const uint8_t* data) +{ + assert(frames >= _latest_event); + + LV2_Atom* atom = get<LV2_Atom>(); + if (atom->type == _factory.uris().atom_Chunk) { + clear(); // Chunk initialized with prepare_output_write(), clear + } + + if (sizeof(LV2_Atom) + atom->size + lv2_atom_pad_size(size) > _capacity) { + return false; + } + + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)atom; + LV2_Atom_Event* ev = (LV2_Atom_Event*)( + (uint8_t*)seq + lv2_atom_total_size(&seq->atom)); + + ev->time.frames = frames; + ev->body.size = size; + ev->body.type = type; + memcpy(ev + 1, data, size); + + atom->size += sizeof(LV2_Atom_Event) + lv2_atom_pad_size(size); + + _latest_event = frames; + + return true; +} + +bool +Buffer::append_event(int64_t frames, const LV2_Atom* body) +{ + return append_event(frames, body->size, body->type, (const uint8_t*)(body + 1)); +} + +bool +Buffer::append_event_buffer(const Buffer* buf) +{ + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)get<LV2_Atom>(); + LV2_Atom_Sequence* bseq = (LV2_Atom_Sequence*)buf->get<LV2_Atom>(); + if (seq->atom.type == _factory.uris().atom_Chunk) { + clear(); // Chunk initialized with prepare_output_write(), clear + } + + const uint32_t total_size = lv2_atom_total_size(&seq->atom); + uint8_t* const end = (uint8_t*)seq + total_size; + const uint32_t n_bytes = bseq->atom.size - sizeof(bseq->body); + if (sizeof(LV2_Atom) + total_size + n_bytes >= _capacity) { + return false; // Not enough space + } + + memcpy(end, bseq + 1, n_bytes); + seq->atom.size += n_bytes; + + _latest_event = std::max(_latest_event, buf->_latest_event); + + return true; +} + +SampleCount +Buffer::next_value_offset(SampleCount offset, SampleCount end) const +{ + if (_type == _factory.uris().atom_Sequence && _value_type) { + const LV2_Atom_Sequence* seq = get<const LV2_Atom_Sequence>(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->time.frames > offset && + ev->time.frames < end && + ev->body.type == _value_type) { + return ev->time.frames; + } + } + } + + /* For CV buffers, it's possible to scan for a value change here, which for + stepped CV would do the right thing, but in the worst case (e.g. with + sine waves), when connected to a control port would split the cycle for + every frame which isn't feasible. Instead, just return end, so the + cycle will not be split. + + A plugin that takes CV and emits discrete change events, possibly with a + maximum rate or fuzz factor, would allow the user to choose which + behaviour, at the cost of some overhead. + */ + + return end; +} + +const LV2_Atom* +Buffer::value() const +{ + return _value_buffer ? _value_buffer->get<const LV2_Atom>() : nullptr; +} + +void +Buffer::set_value(const Atom& value) +{ + if (!value.is_valid() || !_value_buffer) { + return; + } + + const uint32_t total_size = sizeof(LV2_Atom) + value.size(); + if (total_size > _value_buffer->capacity()) { + _value_buffer = _factory.claim_buffer(value.type(), 0, total_size); + } + + memcpy(_value_buffer->get<LV2_Atom*>(), value.atom(), total_size); +} + +void +Buffer::update_value_buffer(SampleCount offset) +{ + if (!_value_buffer || !_value_type) { + return; + } + + LV2_Atom_Sequence* seq = get<LV2_Atom_Sequence>(); + LV2_Atom_Event* latest = nullptr; + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->time.frames > offset) { + break; + } else if (ev->body.type == _value_type) { + latest = ev; + } + } + + if (latest) { + memcpy(_value_buffer->get<LV2_Atom>(), + &latest->body, + lv2_atom_total_size(&latest->body)); + } +} + +#ifndef NDEBUG +void +Buffer::dump_cv(const RunContext& context) const +{ + float value = samples()[0]; + fprintf(stderr, "{ 0000: %.02f\n", value); + for (uint32_t i = 0; i < context.nframes(); ++i) { + if (samples()[i] != value) { + value = samples()[i]; + fprintf(stderr, " %4d: %.02f\n", i, value); + } + } + fprintf(stderr, "}\n"); +} +#endif + +void* Buffer::aligned_alloc(size_t size) +{ +#ifdef HAVE_POSIX_MEMALIGN + void* buf; + if (!posix_memalign((void**)&buf, 16, size)) { + memset(buf, 0, size); + return buf; + } +#else + return (LV2_buf*)calloc(1, size); +#endif + return nullptr; +} + +void +intrusive_ptr_add_ref(Buffer* b) +{ + b->ref(); +} + +void +intrusive_ptr_release(Buffer* b) +{ + b->deref(); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Buffer.hpp b/src/server/Buffer.hpp new file mode 100644 index 00000000..a95fcd3c --- /dev/null +++ b/src/server/Buffer.hpp @@ -0,0 +1,244 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BUFFER_HPP +#define INGEN_ENGINE_BUFFER_HPP + +#include <atomic> +#include <cassert> + +#include "ingen/types.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "raul/Deletable.hpp" + +#include "BufferFactory.hpp" +#include "PortType.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class BufferFactory; +class Engine; +class RunContext; + +class INGEN_API Buffer +{ +public: + Buffer(BufferFactory& bufs, + LV2_URID type, + LV2_URID value_type, + uint32_t capacity, + bool external = false, + void* buf = nullptr); + + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + + void clear(); + void resize(uint32_t capacity); + void copy(const RunContext& context, const Buffer* src); + void prepare_write(RunContext& context); + + void* port_data(PortType port_type, SampleCount offset); + const void* port_data(PortType port_type, SampleCount offset) const; + + inline LV2_URID type() const { return _type; } + inline LV2_URID value_type() const { return _value_type; } + inline uint32_t capacity() const { return _capacity; } + inline uint32_t size() const { + return is_audio() ? _capacity : sizeof(LV2_Atom) + get<LV2_Atom>()->size; + } + + typedef BufferRef (BufferFactory::*GetFn)(LV2_URID, LV2_URID, uint32_t); + + /** Set the buffer type and optional value type for this buffer. + * + * @param get Called to get auxiliary buffers if necessary. + * @param type Type of buffer. + * @param value_type Type of values in buffer if applicable (for sequences). + */ + void set_type(GetFn get, LV2_URID type, LV2_URID value_type); + + inline bool is_audio() const { + return _type == _factory.uris().atom_Sound; + } + + inline bool is_control() const { + return _type == _factory.uris().atom_Float; + } + + inline bool is_sequence() const { + return _type == _factory.uris().atom_Sequence; + } + + /// Audio or float buffers only + inline const Sample* samples() const { + if (is_control()) { + return (const Sample*)LV2_ATOM_BODY_CONST(get<LV2_Atom_Float>()); + } else if (is_audio()) { + return (const Sample*)_buf; + } + return nullptr; + } + + /// Audio buffers only + inline Sample* samples() { + if (is_control()) { + return (Sample*)LV2_ATOM_BODY(get<LV2_Atom_Float>()); + } else if (is_audio()) { + return (Sample*)_buf; + } + return nullptr; + } + + /// Numeric buffers only + inline Sample value_at(SampleCount offset) const { + if (is_audio() || is_control()) { + return samples()[offset]; + } else if (_value_buffer) { + return ((LV2_Atom_Float*)value())->body; + } + return 0.0f; + } + + inline void set_block(const Sample val, + const SampleCount start, + const SampleCount end) + { + if (is_sequence()) { + append_event(start, sizeof(val), _factory.uris().atom_Float, + reinterpret_cast<const uint8_t*>( + static_cast<const float*>(&val))); + _value_buffer->get<LV2_Atom_Float>()->body = val; + return; + } + + assert(is_audio() || is_control()); + assert(end <= _capacity / sizeof(Sample)); + // Note: Do not change this without ensuring GCC can still vectorize it + Sample* const buf = samples() + start; + for (SampleCount i = 0; i < (end - start); ++i) { + buf[i] = val; + } + } + + inline void add_block(const Sample val, + const SampleCount start, + const SampleCount end) + { + assert(is_audio() || is_control()); + assert(end <= _capacity / sizeof(Sample)); + // Note: Do not change this without ensuring GCC can still vectorize it + Sample* const buf = samples() + start; + for (SampleCount i = 0; i < (end - start); ++i) { + buf[i] += val; + } + } + + inline void write_block(const Sample val, + const SampleCount start, + const SampleCount end, + const bool add) + { + if (add) { + add_block(val, start, end); + } else { + set_block(val, start, end); + } + } + + /// Audio buffers only + float peak(const RunContext& context) const; + + /// Sequence buffers only + void prepare_output_write(RunContext& context); + + /// Sequence buffers only + bool append_event(int64_t frames, + uint32_t size, + uint32_t type, + const uint8_t* data); + + /// Sequence buffers only + bool append_event(int64_t frames, const LV2_Atom* body); + + /// Sequence buffers only + bool append_event_buffer(const Buffer* buf); + + /// Value buffer for numeric sequences + BufferRef value_buffer() { return _value_buffer; } + + /// Return the current value + const LV2_Atom* value() const; + + /// Set/initialise current value in value buffer + void set_value(const Atom& value); + + /// Return offset of the first value change after `offset` + SampleCount next_value_offset(SampleCount offset, SampleCount end) const; + + /// Update value buffer to value as of offset + void update_value_buffer(SampleCount offset); + + /// Set/add to audio buffer from the Sequence of Float in `src` + void render_sequence(const RunContext& context, const Buffer* src, bool add); + +#ifndef NDEBUG + void dump_cv(const RunContext& context) const; +#endif + + void set_capacity(uint32_t capacity) { _capacity = capacity; } + + void set_buffer(void* buf) { assert(_external); _buf = buf; } + + static void* aligned_alloc(size_t size); + + template<typename T> const T* get() const { return reinterpret_cast<const T*>(_buf); } + template<typename T> T* get() { return reinterpret_cast<T*>(_buf); } + + inline void ref() { ++_refs; } + + inline void deref() { + if ((--_refs) == 0) { + recycle(); + } + } + +private: + friend class BufferFactory; + ~Buffer(); + + void recycle(); + + BufferFactory& _factory; + Buffer* _next; ///< Intrusive linked list for BufferFactory + void* _buf; ///< Actual buffer memory + BufferRef _value_buffer; ///< Value buffer for numeric sequences + int64_t _latest_event; + LV2_URID _type; + LV2_URID _value_type; + uint32_t _capacity; + std::atomic<unsigned> _refs; ///< Intrusive reference count + bool _external; ///< Buffer is externally allocated +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BUFFER_HPP diff --git a/src/server/BufferFactory.cpp b/src/server/BufferFactory.cpp new file mode 100644 index 00000000..d5d947d0 --- /dev/null +++ b/src/server/BufferFactory.cpp @@ -0,0 +1,190 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" + +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" + +namespace Ingen { +namespace Server { + +BufferFactory::BufferFactory(Engine& engine, URIs& uris) + : _free_audio(nullptr) + , _free_control(nullptr) + , _free_sequence(nullptr) + , _free_object(nullptr) + , _engine(engine) + , _uris(uris) + , _seq_size(0) + , _silent_buffer(nullptr) +{ +} + +BufferFactory::~BufferFactory() +{ + _silent_buffer.reset(); + free_list(_free_audio.load()); + free_list(_free_control.load()); + free_list(_free_sequence.load()); + free_list(_free_object.load()); +} + +Forge& +BufferFactory::forge() +{ + return _engine.world()->forge(); +} + +Raul::Maid& +BufferFactory::maid() +{ + return *_engine.maid(); +} + +void +BufferFactory::free_list(Buffer* head) +{ + while (head) { + Buffer* next = head->_next; + delete head; + head = next; + } +} + +void +BufferFactory::set_block_length(SampleCount block_length) +{ + _silent_buffer = create(_uris.atom_Sound, audio_buffer_size(block_length)); +} + +uint32_t +BufferFactory::audio_buffer_size(SampleCount nframes) +{ + return nframes * sizeof(Sample); +} + +uint32_t +BufferFactory::audio_buffer_size() const +{ + return _engine.block_length() * sizeof(Sample); +} + +uint32_t +BufferFactory::default_size(LV2_URID type) const +{ + if (type == _uris.atom_Float) { + return sizeof(LV2_Atom_Float); + } else if (type == _uris.atom_Sound) { + return audio_buffer_size(_engine.block_length()); + } else if (type == _uris.atom_URID) { + return sizeof(LV2_Atom_URID); + } else if (type == _uris.atom_Sequence) { + if (_seq_size == 0) { + return _engine.sequence_size(); + } else { + return _seq_size; + } + } else { + return 0; + } +} + +Buffer* +BufferFactory::try_get_buffer(LV2_URID type) +{ + std::atomic<Buffer*>& head_ptr = free_list(type); + Buffer* head = nullptr; + Buffer* next; + do { + head = head_ptr.load(); + if (!head) { + break; + } + next = head->_next; + } while (!head_ptr.compare_exchange_weak(head, next)); + + return head; +} + +BufferRef +BufferFactory::get_buffer(LV2_URID type, + LV2_URID value_type, + uint32_t capacity) +{ + Buffer* try_head = try_get_buffer(type); + if (!try_head) { + return create(type, value_type, capacity); + } + + try_head->_next = nullptr; + try_head->set_type(&BufferFactory::get_buffer, type, value_type); + try_head->clear(); + return BufferRef(try_head); +} + +BufferRef +BufferFactory::claim_buffer(LV2_URID type, + LV2_URID value_type, + uint32_t capacity) +{ + Buffer* try_head = try_get_buffer(type); + if (!try_head) { + _engine.world()->log().rt_error("Failed to obtain buffer"); + return BufferRef(); + } + + try_head->_next = nullptr; + try_head->set_type(&BufferFactory::claim_buffer, type, value_type); + return BufferRef(try_head); +} + +BufferRef +BufferFactory::silent_buffer() +{ + return _silent_buffer; +} + +BufferRef +BufferFactory::create(LV2_URID type, LV2_URID value_type, uint32_t capacity) +{ + if (capacity == 0) { + capacity = default_size(type); + } else if (type == _uris.atom_Float) { + capacity = std::max(capacity, (uint32_t)sizeof(LV2_Atom_Float)); + } else if (type == _uris.atom_Sound) { + capacity = std::max(capacity, default_size(_uris.atom_Sound)); + } + + return BufferRef(new Buffer(*this, type, value_type, capacity)); +} + +void +BufferFactory::recycle(Buffer* buf) +{ + std::atomic<Buffer*>& head_ptr = free_list(buf->type()); + Buffer* try_head; + do { + try_head = head_ptr.load(); + buf->_next = try_head; + } while (!head_ptr.compare_exchange_weak(try_head, buf)); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/BufferFactory.hpp b/src/server/BufferFactory.hpp new file mode 100644 index 00000000..8265fc98 --- /dev/null +++ b/src/server/BufferFactory.hpp @@ -0,0 +1,118 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BUFFERFACTORY_HPP +#define INGEN_ENGINE_BUFFERFACTORY_HPP + +#include <atomic> +#include <map> +#include <mutex> + +#include "ingen/Atom.hpp" +#include "ingen/Forge.hpp" +#include "ingen/URIs.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "raul/RingBuffer.hpp" + +#include "BufferRef.hpp" +#include "PortType.hpp" +#include "types.hpp" + +namespace Raul { class Maid; } + +namespace Ingen { + +class URIs; + +namespace Server { + +class Engine; + +class INGEN_API BufferFactory { +public: + BufferFactory(Engine& engine, URIs& uris); + ~BufferFactory(); + + static uint32_t audio_buffer_size(SampleCount nframes); + + uint32_t audio_buffer_size() const; + uint32_t default_size(LV2_URID type) const; + + /** Dynamically allocate a new Buffer. */ + BufferRef create(LV2_URID type, + LV2_URID value_type, + uint32_t capacity = 0); + + /** Get a new buffer, reusing if possible, allocating if otherwise. */ + BufferRef get_buffer(LV2_URID type, + LV2_URID value_type, + uint32_t capacity); + + /** Claim an existing buffer, never allocates, real-time safe. */ + BufferRef claim_buffer(LV2_URID type, + LV2_URID value_type, + uint32_t capacity); + + /** Return a reference to a shared silent buffer. */ + BufferRef silent_buffer(); + + void set_block_length(SampleCount block_length); + void set_seq_size(uint32_t seq_size) { _seq_size = seq_size; } + + Forge& forge(); + Raul::Maid& maid(); + + URIs& uris() { return _uris; } + Engine& engine() { return _engine; } + +private: + friend class Buffer; + void recycle(Buffer* buf); + + Buffer* try_get_buffer(LV2_URID type); + + inline std::atomic<Buffer*>& free_list(LV2_URID type) { + if (type == _uris.atom_Float) { + return _free_control; + } else if (type == _uris.atom_Sound) { + return _free_audio; + } else if (type == _uris.atom_Sequence) { + return _free_sequence; + } else { + return _free_object; + } + } + + void free_list(Buffer* head); + + std::atomic<Buffer*> _free_audio; + std::atomic<Buffer*> _free_control; + std::atomic<Buffer*> _free_sequence; + std::atomic<Buffer*> _free_object; + + std::mutex _mutex; + Engine& _engine; + URIs& _uris; + uint32_t _seq_size; + + BufferRef _silent_buffer; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BUFFERFACTORY_HPP diff --git a/src/server/BufferRef.hpp b/src/server/BufferRef.hpp new file mode 100644 index 00000000..2a1cbc27 --- /dev/null +++ b/src/server/BufferRef.hpp @@ -0,0 +1,38 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BUFFER_REF_HPP +#define INGEN_ENGINE_BUFFER_REF_HPP + +#include <boost/intrusive_ptr.hpp> + +#include "ingen/ingen.h" + +namespace Ingen { +namespace Server { + +class Buffer; + +typedef boost::intrusive_ptr<Buffer> BufferRef; + +// Defined in Buffer.cpp +INGEN_API void intrusive_ptr_add_ref(Buffer* b); +INGEN_API void intrusive_ptr_release(Buffer* b); + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BUFFER_REF_HPP diff --git a/src/server/ClientUpdate.cpp b/src/server/ClientUpdate.cpp new file mode 100644 index 00000000..60dd02e3 --- /dev/null +++ b/src/server/ClientUpdate.cpp @@ -0,0 +1,155 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Interface.hpp" +#include "ingen/URIs.hpp" + +#include "BlockImpl.hpp" +#include "BufferFactory.hpp" +#include "ClientUpdate.hpp" +#include "GraphImpl.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +void +ClientUpdate::put(const URI& uri, + const Properties& props, + Resource::Graph ctx) +{ + const ClientUpdate::Put put = { uri, props, ctx }; + puts.push_back(put); +} + +void +ClientUpdate::put_port(const PortImpl* port) +{ + const URIs& uris = port->bufs().uris(); + if (port->is_a(PortType::CONTROL) || port->is_a(PortType::CV)) { + Properties props = port->properties(); + props.erase(uris.ingen_value); + props.emplace(uris.ingen_value, port->value()); + put(port->uri(), props); + } else { + put(port->uri(), port->properties()); + } +} + +void +ClientUpdate::put_block(const BlockImpl* block) +{ + const PluginImpl* const plugin = block->plugin_impl(); + const URIs& uris = plugin->uris(); + + if (uris.ingen_Graph == plugin->type()) { + put_graph((const GraphImpl*)block); + } else { + put(block->uri(), block->properties()); + for (size_t j = 0; j < block->num_ports(); ++j) { + put_port(block->port_impl(j)); + } + } +} + +void +ClientUpdate::put_graph(const GraphImpl* graph) +{ + put(graph->uri(), + graph->properties(Resource::Graph::INTERNAL), + Resource::Graph::INTERNAL); + + put(graph->uri(), + graph->properties(Resource::Graph::EXTERNAL), + Resource::Graph::EXTERNAL); + + // Enqueue blocks + for (const auto& b : graph->blocks()) { + put_block(&b); + } + + // Enqueue ports + for (uint32_t i = 0; i < graph->num_ports_non_rt(); ++i) { + put_port(graph->port_impl(i)); + } + + // Enqueue arcs + for (const auto& a : graph->arcs()) { + const SPtr<const Arc> arc = a.second; + const Connect connect = { arc->tail_path(), arc->head_path() }; + connects.push_back(connect); + } +} + +void +ClientUpdate::put_plugin(PluginImpl* plugin) +{ + put(plugin->uri(), plugin->properties()); + + for (const auto& p : plugin->presets()) { + put_preset(plugin->uris(), plugin->uri(), p.first, p.second); + } +} + +void +ClientUpdate::put_preset(const URIs& uris, + const URI& plugin, + const URI& preset, + const std::string& label) +{ + const Properties props{ + { uris.rdf_type, uris.pset_Preset.urid }, + { uris.rdfs_label, uris.forge.alloc(label) }, + { uris.lv2_appliesTo, uris.forge.make_urid(plugin) }}; + put(preset, props); +} + +void +ClientUpdate::del(const URI& subject) +{ + dels.push_back(subject); +} + +/** Returns true if a is closer to the root than b. */ +static inline bool +put_higher_than(const ClientUpdate::Put& a, const ClientUpdate::Put& b) +{ + return (std::count(a.uri.begin(), a.uri.end(), '/') < + std::count(b.uri.begin(), b.uri.end(), '/')); +} + +void +ClientUpdate::send(Interface& dest) +{ + // Send deletions + for (const URI& subject : dels) { + dest.del(subject); + } + + // Send puts in increasing depth order so parents are sent first + std::stable_sort(puts.begin(), puts.end(), put_higher_than); + for (const ClientUpdate::Put& put : puts) { + dest.put(put.uri, put.properties, put.ctx); + } + + // Send connections + for (const ClientUpdate::Connect& connect : connects) { + dest.connect(connect.tail, connect.head); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/ClientUpdate.hpp b/src/server/ClientUpdate.hpp new file mode 100644 index 00000000..f1a361f7 --- /dev/null +++ b/src/server/ClientUpdate.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_CLIENTUPDATE_HPP +#define INGEN_ENGINE_CLIENTUPDATE_HPP + +#include <string> +#include <vector> + +#include "ingen/Resource.hpp" +#include "raul/Path.hpp" + +namespace Ingen { + +class Interface; +class URIs; + +namespace Server { + +class PortImpl; +class BlockImpl; +class GraphImpl; +class PluginImpl; + +/** A sequence of puts/connects/deletes to update clients. + * + * Events like Get construct this in pre_process() and later send it in + * post_process() to avoid the need to lock. + */ +struct ClientUpdate { + void put(const URI& uri, + const Properties& props, + Resource::Graph ctx = Resource::Graph::DEFAULT); + + void put_port(const PortImpl* port); + void put_block(const BlockImpl* block); + void put_graph(const GraphImpl* graph); + void put_plugin(PluginImpl* plugin); + void put_preset(const URIs& uris, + const URI& plugin, + const URI& preset, + const std::string& label); + + void del(const URI& subject); + + void send(Interface& dest); + + struct Put { + URI uri; + Properties properties; + Resource::Graph ctx; + }; + + struct Connect { + Raul::Path tail; + Raul::Path head; + }; + + std::vector<URI> dels; + std::vector<Put> puts; + std::vector<Connect> connects; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_CLIENTUPDATE_HPP diff --git a/src/server/CompiledGraph.cpp b/src/server/CompiledGraph.cpp new file mode 100644 index 00000000..35b07935 --- /dev/null +++ b/src/server/CompiledGraph.cpp @@ -0,0 +1,274 @@ +/* + This file is part of Ingen. + Copyright 2015-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <algorithm> + +#include "ingen/ColorContext.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/World.hpp" + +#include "CompiledGraph.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +/** Graph contains ambiguous feedback with no delay nodes. */ +class FeedbackException : public std::exception { +public: + FeedbackException(const BlockImpl* node, const BlockImpl* root=nullptr) + : node(node) + , root(root) + {} + + const BlockImpl* node; + const BlockImpl* root; +}; + +static bool +has_provider_with_many_dependants(BlockImpl* n) +{ + for (BlockImpl* p : n->providers()) { + if (p->dependants().size() > 1) { + return true; + } + } + + return false; +} + +CompiledGraph::CompiledGraph(GraphImpl* graph) + : _master(std::unique_ptr<Task>(new Task(Task::Mode::SEQUENTIAL))) +{ + compile_graph(graph); +} + +MPtr<CompiledGraph> +CompiledGraph::compile(Raul::Maid& maid, GraphImpl& graph) +{ + try { + return maid.make_managed<CompiledGraph>(&graph); + } catch (const FeedbackException& e) { + Log& log = graph.engine().log(); + if (e.node && e.root) { + log.error(fmt("Feedback compiling %1% from %2%\n") + % e.node->path() % e.root->path()); + } else { + log.error(fmt("Feedback compiling %1%\n") + % e.node->path()); + } + return MPtr<CompiledGraph>(); + } +} + +static size_t +num_unvisited_dependants(BlockImpl* block) +{ + size_t count = 0; + for (BlockImpl* b : block->dependants()) { + if (b->get_mark() == BlockImpl::Mark::UNVISITED) { + ++count; + } + } + return count; +} + +static size_t +parallel_depth(BlockImpl* block) +{ + if (has_provider_with_many_dependants(block)) { + return 2; + } + + size_t min_provider_depth = std::numeric_limits<size_t>::max(); + for (auto p : block->providers()) { + min_provider_depth = std::min(min_provider_depth, parallel_depth(p)); + } + + return 2 + min_provider_depth; +} + +void +CompiledGraph::compile_graph(GraphImpl* graph) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + // Start with sink nodes (no outputs, or connected only to graph outputs) + std::set<BlockImpl*> blocks; + for (auto& b : graph->blocks()) { + // Mark all blocks as unvisited initially + b.set_mark(BlockImpl::Mark::UNVISITED); + + if (b.dependants().empty()) { + // Block has no dependants, add to initial working set + blocks.insert(&b); + } + } + + // Keep compiling working set until all nodes are visited + while (!blocks.empty()) { + std::set<BlockImpl*> predecessors; + + // Calculate maximum sequential depth to consume this phase + size_t depth = std::numeric_limits<size_t>::max(); + for (auto i : blocks) { + depth = std::min(depth, parallel_depth(i)); + } + + Task par(Task::Mode::PARALLEL); + for (auto b : blocks) { + assert(num_unvisited_dependants(b) == 0); + Task seq(Task::Mode::SEQUENTIAL); + compile_block(b, seq, depth, predecessors); + par.push_front(std::move(seq)); + } + _master->push_front(std::move(par)); + blocks = predecessors; + } + + _master = Task::simplify(std::move(_master)); + + if (graph->engine().world()->conf().option("trace").get<int32_t>()) { + ColorContext ctx(stderr, ColorContext::Color::YELLOW); + dump(graph->path()); + } +} + +/** Throw a FeedbackException iff `dependant` has `root` as a dependency. */ +static void +check_feedback(const BlockImpl* root, BlockImpl* provider) +{ + if (provider == root) { + throw FeedbackException(root); + } + + for (auto p : provider->providers()) { + const BlockImpl::Mark mark = p->get_mark(); + switch (mark) { + case BlockImpl::Mark::UNVISITED: + p->set_mark(BlockImpl::Mark::VISITING); + check_feedback(root, p); + break; + case BlockImpl::Mark::VISITING: + throw FeedbackException(p, root); + case BlockImpl::Mark::VISITED: + break; + } + p->set_mark(mark); + } +} + +void +CompiledGraph::compile_provider(const BlockImpl* root, + BlockImpl* block, + Task& task, + size_t max_depth, + std::set<BlockImpl*>& k) +{ + if (block->dependants().size() > 1) { + /* Provider has other dependants, so this is the tail of a sequential task. + Add provider to future working set and stop traversal. */ + check_feedback(root, block); + if (num_unvisited_dependants(block) == 0) { + k.insert(block); + } + } else if (max_depth > 0) { + // Calling dependant has only this provider, add here + if (task.mode() == Task::Mode::PARALLEL) { + // Inside a parallel task, compile into a new sequential child + Task seq(Task::Mode::SEQUENTIAL); + compile_block(block, seq, max_depth, k); + task.push_front(std::move(seq)); + } else { + // Prepend to given sequential task + compile_block(block, task, max_depth, k); + } + } else { + if (num_unvisited_dependants(block) == 0) { + k.insert(block); + } + } +} + +void +CompiledGraph::compile_block(BlockImpl* n, + Task& task, + size_t max_depth, + std::set<BlockImpl*>& k) +{ + switch (n->get_mark()) { + case BlockImpl::Mark::UNVISITED: + n->set_mark(BlockImpl::Mark::VISITING); + + // Execute this task after the providers to follow + task.push_front(Task(Task::Mode::SINGLE, n)); + + if (n->providers().size() < 2) { + // Single provider, prepend it to this sequential task + for (auto p : n->providers()) { + compile_provider(n, p, task, max_depth - 1, k); + } + } else if (has_provider_with_many_dependants(n)) { + // Stop recursion and enqueue providers for the next round + for (auto p : n->providers()) { + if (num_unvisited_dependants(p) == 0) { + k.insert(p); + } + } + } else { + // Multiple providers with only this node as dependant, + // make a new parallel task to execute them + Task par(Task::Mode::PARALLEL); + for (auto p : n->providers()) { + compile_provider(n, p, par, max_depth - 1, k); + } + task.push_front(std::move(par)); + } + n->set_mark(BlockImpl::Mark::VISITED); + break; + + case BlockImpl::Mark::VISITING: + throw FeedbackException(n); + + case BlockImpl::Mark::VISITED: + break; + } +} + +void +CompiledGraph::run(RunContext& context) +{ + _master->run(context); +} + +void +CompiledGraph::dump(const std::string& name) const +{ + auto sink = [](const std::string& s) { + fwrite(s.c_str(), 1, s.size(), stderr); + }; + + sink("(compiled-graph "); + sink(name); + _master->dump(sink, 2, false); + sink(")\n"); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/CompiledGraph.hpp b/src/server/CompiledGraph.hpp new file mode 100644 index 00000000..6b802611 --- /dev/null +++ b/src/server/CompiledGraph.hpp @@ -0,0 +1,84 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_COMPILEDGRAPH_HPP +#define INGEN_ENGINE_COMPILEDGRAPH_HPP + +#include <functional> +#include <set> +#include <vector> + +#include "ingen/types.hpp" +#include "raul/Maid.hpp" +#include "raul/Noncopyable.hpp" + +#include "Task.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; +class RunContext; + +/** A graph ``compiled'' into a quickly executable form. + * + * This is a flat sequence of nodes ordered such that the process thread can + * execute the nodes in order and have nodes always executed before any of + * their dependencies. + */ +class CompiledGraph : public Raul::Maid::Disposable + , public Raul::Noncopyable +{ +public: + static MPtr<CompiledGraph> compile(Raul::Maid& maid, GraphImpl& graph); + + void run(RunContext& context); + +private: + friend class Raul::Maid; ///< Allow make_managed to construct + + CompiledGraph(GraphImpl* graph); + + typedef std::set<BlockImpl*> BlockSet; + + void dump(const std::string& name) const; + + void compile_graph(GraphImpl* graph); + + void compile_block(BlockImpl* n, + Task& task, + size_t max_depth, + BlockSet& k); + + void compile_provider(const BlockImpl* root, + BlockImpl* block, + Task& task, + size_t max_depth, + BlockSet& k); + + std::unique_ptr<Task> _master; +}; + +inline MPtr<CompiledGraph> compile(Raul::Maid& maid, GraphImpl& graph) +{ + return CompiledGraph::compile(maid, graph); +} + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_COMPILEDGRAPH_HPP diff --git a/src/server/ControlBindings.cpp b/src/server/ControlBindings.cpp new file mode 100644 index 00000000..3901d1c2 --- /dev/null +++ b/src/server/ControlBindings.cpp @@ -0,0 +1,425 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "Buffer.hpp" +#include "ControlBindings.hpp" +#include "Engine.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +ControlBindings::ControlBindings(Engine& engine) + : _engine(engine) + , _learn_binding(nullptr) + , _bindings(new Bindings()) + , _feedback(new Buffer(*_engine.buffer_factory(), + engine.world()->uris().atom_Sequence, + 0, + 4096)) // FIXME: capacity? +{ + lv2_atom_forge_init( + &_forge, &engine.world()->uri_map().urid_map_feature()->urid_map); +} + +ControlBindings::~ControlBindings() +{ + _feedback.reset(); + delete _learn_binding.load(); +} + +ControlBindings::Key +ControlBindings::port_binding(PortImpl* port) const +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + const Ingen::URIs& uris = _engine.world()->uris(); + const Atom& binding = port->get_property(uris.midi_binding); + return binding_key(binding); +} + +ControlBindings::Key +ControlBindings::binding_key(const Atom& binding) const +{ + const Ingen::URIs& uris = _engine.world()->uris(); + Key key; + LV2_Atom* num = nullptr; + if (binding.type() == uris.atom_Object) { + const LV2_Atom_Object_Body* obj = (const LV2_Atom_Object_Body*) + binding.get_body(); + if (obj->otype == uris.midi_Bender) { + key = Key(Type::MIDI_BENDER); + } else if (obj->otype == uris.midi_ChannelPressure) { + key = Key(Type::MIDI_CHANNEL_PRESSURE); + } else if (obj->otype == uris.midi_Controller) { + lv2_atom_object_body_get( + binding.size(), obj, (LV2_URID)uris.midi_controllerNumber, &num, NULL); + if (!num) { + _engine.log().rt_error("Controller binding missing number\n"); + } else if (num->type != uris.atom_Int) { + _engine.log().rt_error("Controller number not an integer\n"); + } else { + key = Key(Type::MIDI_CC, ((LV2_Atom_Int*)num)->body); + } + } else if (obj->otype == uris.midi_NoteOn) { + lv2_atom_object_body_get( + binding.size(), obj, (LV2_URID)uris.midi_noteNumber, &num, NULL); + if (!num) { + _engine.log().rt_error("Note binding missing number\n"); + } else if (num->type != uris.atom_Int) { + _engine.log().rt_error("Note number not an integer\n"); + } else { + key = Key(Type::MIDI_NOTE, ((LV2_Atom_Int*)num)->body); + } + } + } else if (binding.type()) { + _engine.log().rt_error("Unknown binding type\n"); + } + return key; +} + +ControlBindings::Key +ControlBindings::midi_event_key(uint16_t size, const uint8_t* buf, uint16_t& value) +{ + switch (lv2_midi_message_type(buf)) { + case LV2_MIDI_MSG_CONTROLLER: + value = static_cast<int8_t>(buf[2]); + return Key(Type::MIDI_CC, static_cast<int8_t>(buf[1])); + case LV2_MIDI_MSG_BENDER: + value = (static_cast<int8_t>(buf[2]) << 7) + static_cast<int8_t>(buf[1]); + return Key(Type::MIDI_BENDER); + case LV2_MIDI_MSG_CHANNEL_PRESSURE: + value = static_cast<int8_t>(buf[1]); + return Key(Type::MIDI_CHANNEL_PRESSURE); + case LV2_MIDI_MSG_NOTE_ON: + value = 1.0f; + return Key(Type::MIDI_NOTE, static_cast<int8_t>(buf[1])); + default: + return Key(); + } +} + +bool +ControlBindings::set_port_binding(RunContext& context, + PortImpl* port, + Binding* binding, + const Atom& value) +{ + const Key key = binding_key(value); + if (!!key) { + binding->key = key; + binding->port = port; + _bindings->insert(*binding); + return true; + } else { + return false; + } +} + +void +ControlBindings::port_value_changed(RunContext& ctx, + PortImpl* port, + Key key, + const Atom& value_atom) +{ + Ingen::World* world = ctx.engine().world(); + const Ingen::URIs& uris = world->uris(); + if (!!key) { + int16_t value = port_value_to_control( + ctx, port, key.type, value_atom); + uint16_t size = 0; + uint8_t buf[4]; + switch (key.type) { + case Type::MIDI_CC: + size = 3; + buf[0] = LV2_MIDI_MSG_CONTROLLER; + buf[1] = key.num; + buf[2] = static_cast<int8_t>(value); + break; + case Type::MIDI_CHANNEL_PRESSURE: + size = 2; + buf[0] = LV2_MIDI_MSG_CHANNEL_PRESSURE; + buf[1] = static_cast<int8_t>(value); + break; + case Type::MIDI_BENDER: + size = 3; + buf[0] = LV2_MIDI_MSG_BENDER; + buf[1] = (value & 0x007F); + buf[2] = (value & 0x7F00) >> 7; + break; + case Type::MIDI_NOTE: + size = 3; + if (value == 1) { + buf[0] = LV2_MIDI_MSG_NOTE_ON; + } else if (value == 0) { + buf[0] = LV2_MIDI_MSG_NOTE_OFF; + } + buf[1] = key.num; + buf[2] = 0x64; // MIDI spec default + break; + default: + break; + } + if (size > 0) { + _feedback->append_event(ctx.nframes() - 1, size, (LV2_URID)uris.midi_MidiEvent, buf); + } + } +} + +void +ControlBindings::start_learn(PortImpl* port) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + Binding* b = _learn_binding.load(); + if (!b) { + _learn_binding = new Binding(Type::NULL_CONTROL, port); + } else { + b->port = port; + } +} + +static void +get_range(RunContext& context, const PortImpl* port, float* min, float* max) +{ + *min = port->minimum().get<float>(); + *max = port->maximum().get<float>(); + if (port->is_sample_rate()) { + *min *= context.engine().sample_rate(); + *max *= context.engine().sample_rate(); + } +} + +float +ControlBindings::control_to_port_value(RunContext& context, + const PortImpl* port, + Type type, + int16_t value) const +{ + float normal = 0.0f; + switch (type) { + case Type::MIDI_CC: + case Type::MIDI_CHANNEL_PRESSURE: + normal = (float)value / 127.0f; + break; + case Type::MIDI_BENDER: + normal = (float)value / 16383.0f; + break; + case Type::MIDI_NOTE: + normal = (value == 0.0f) ? 0.0f : 1.0f; + break; + default: + break; + } + + if (port->is_logarithmic()) { + normal = (expf(normal) - 1.0f) / ((float)M_E - 1.0f); + } + + float min, max; + get_range(context, port, &min, &max); + + return normal * (max - min) + min; +} + +int16_t +ControlBindings::port_value_to_control(RunContext& context, + PortImpl* port, + Type type, + const Atom& value_atom) const +{ + if (value_atom.type() != port->bufs().forge().Float) { + return 0; + } + + float min, max; + get_range(context, port, &min, &max); + + const float value = value_atom.get<float>(); + float normal = (value - min) / (max - min); + + if (normal < 0.0f) { + normal = 0.0f; + } + + if (normal > 1.0f) { + normal = 1.0f; + } + + if (port->is_logarithmic()) { + normal = logf(normal * ((float)M_E - 1.0f) + 1.0); + } + + switch (type) { + case Type::MIDI_CC: + case Type::MIDI_CHANNEL_PRESSURE: + return lrintf(normal * 127.0f); + case Type::MIDI_BENDER: + return lrintf(normal * 16383.0f); + case Type::MIDI_NOTE: + return (value > 0.0f) ? 1 : 0; + default: + return 0; + } +} + +static void +forge_binding(const URIs& uris, + LV2_Atom_Forge* forge, + ControlBindings::Type binding_type, + int32_t value) +{ + LV2_Atom_Forge_Frame frame; + switch (binding_type) { + case ControlBindings::Type::MIDI_CC: + lv2_atom_forge_object(forge, &frame, 0, uris.midi_Controller); + lv2_atom_forge_key(forge, uris.midi_controllerNumber); + lv2_atom_forge_int(forge, value); + break; + case ControlBindings::Type::MIDI_BENDER: + lv2_atom_forge_object(forge, &frame, 0, uris.midi_Bender); + break; + case ControlBindings::Type::MIDI_CHANNEL_PRESSURE: + lv2_atom_forge_object(forge, &frame, 0, uris.midi_ChannelPressure); + break; + case ControlBindings::Type::MIDI_NOTE: + lv2_atom_forge_object(forge, &frame, 0, uris.midi_NoteOn); + lv2_atom_forge_key(forge, uris.midi_noteNumber); + lv2_atom_forge_int(forge, value); + break; + case ControlBindings::Type::MIDI_RPN: // TODO + case ControlBindings::Type::MIDI_NRPN: // TODO + case ControlBindings::Type::NULL_CONTROL: + break; + } +} + +void +ControlBindings::set_port_value(RunContext& context, + PortImpl* port, + Type type, + int16_t value) +{ + float min, max; + get_range(context, port, &min, &max); + + const float val = control_to_port_value(context, port, type, value); + + // TODO: Set port value property so it is saved + port->set_control_value(context, context.start(), val); + + URIs& uris = context.engine().world()->uris(); + context.notify(uris.ingen_value, context.start(), port, + sizeof(float), _forge.Float, &val); +} + +bool +ControlBindings::finish_learn(RunContext& context, Key key) +{ + const Ingen::URIs& uris = context.engine().world()->uris(); + Binding* binding = _learn_binding.exchange(nullptr); + if (!binding || (key.type == Type::MIDI_NOTE && !binding->port->is_toggled())) { + return false; + } + + binding->key = key; + _bindings->insert(*binding); + + LV2_Atom buf[16]; + memset(buf, 0, sizeof(buf)); + lv2_atom_forge_set_buffer(&_forge, (uint8_t*)buf, sizeof(buf)); + forge_binding(uris, &_forge, key.type, key.num); + const LV2_Atom* atom = buf; + context.notify(uris.midi_binding, + context.start(), + binding->port, + atom->size, atom->type, LV2_ATOM_BODY_CONST(atom)); + + return true; +} + +void +ControlBindings::get_all(const Raul::Path& path, std::vector<Binding*>& bindings) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + for (Binding& b : *_bindings) { + if (b.port->path() == path || b.port->path().is_child_of(path)) { + bindings.push_back(&b); + } + } +} + +void +ControlBindings::remove(RunContext& ctx, const std::vector<Binding*>& bindings) +{ + for (Binding* b : bindings) { + _bindings->erase(*b); + } +} + +void +ControlBindings::pre_process(RunContext& ctx, Buffer* buffer) +{ + uint16_t value = 0; + Ingen::World* world = ctx.engine().world(); + const Ingen::URIs& uris = world->uris(); + + _feedback->clear(); + if ((!_learn_binding && _bindings->empty()) || !buffer->get<LV2_Atom>()) { + return; // Don't bother reading input + } + + LV2_Atom_Sequence* seq = buffer->get<LV2_Atom_Sequence>(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->body.type == uris.midi_MidiEvent) { + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY(&ev->body); + const Key key = midi_event_key(ev->body.size, buf, value); + + if (_learn_binding && !!key) { + finish_learn(ctx, key); // Learn new binding + } + + // Set all controls bound to this key + const Binding k = {key, nullptr}; + for (Bindings::const_iterator i = _bindings->lower_bound(k); + i != _bindings->end() && i->key == key; + ++i) { + set_port_value(ctx, i->port, key.type, value); + } + } + } +} + +void +ControlBindings::post_process(RunContext& context, Buffer* buffer) +{ + if (buffer->get<LV2_Atom>()) { + buffer->append_event_buffer(_feedback.get()); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/ControlBindings.hpp b/src/server/ControlBindings.hpp new file mode 100644 index 00000000..3160f8b2 --- /dev/null +++ b/src/server/ControlBindings.hpp @@ -0,0 +1,148 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_CONTROLBINDINGS_HPP +#define INGEN_ENGINE_CONTROLBINDINGS_HPP + +#include <atomic> +#include <cstdint> +#include <vector> + +#include <boost/intrusive/options.hpp> +#include <boost/intrusive/set.hpp> + +#include "ingen/Atom.hpp" +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "BufferFactory.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class RunContext; +class PortImpl; + +class ControlBindings { +public: + enum class Type : uint16_t { + NULL_CONTROL, + MIDI_BENDER, + MIDI_CC, + MIDI_RPN, + MIDI_NRPN, + MIDI_CHANNEL_PRESSURE, + MIDI_NOTE + }; + + struct Key { + Key(Type t=Type::NULL_CONTROL, int16_t n=0) : type(t), num(n) {} + inline bool operator<(const Key& other) const { + return ((type < other.type) || + (type == other.type && num < other.num)); + } + inline bool operator==(const Key& other) const { + return type == other.type && num == other.num; + } + inline bool operator!() const { return type == Type::NULL_CONTROL; } + Type type; + int16_t num; + }; + + /** One binding of a controller to a port. */ + struct Binding : public boost::intrusive::set_base_hook<>, + public Raul::Maid::Disposable { + Binding(Key k=Key(), PortImpl* p=nullptr) : key(std::move(k)), port(p) {} + + inline bool operator<(const Binding& rhs) const { return key < rhs.key; } + + Key key; + PortImpl* port; + }; + + /** Comparator for bindings by key. */ + struct BindingLess { + bool operator()(const Binding& lhs, const Binding& rhs) const { + return lhs.key < rhs.key; + } + }; + + explicit ControlBindings(Engine& engine); + ~ControlBindings(); + + Key port_binding(PortImpl* port) const; + Key binding_key(const Atom& binding) const; + + void start_learn(PortImpl* port); + + /** Set the binding for `port` to `binding` and take ownership of it. */ + bool set_port_binding(RunContext& ctx, + PortImpl* port, + Binding* binding, + const Atom& value); + + void port_value_changed(RunContext& ctx, + PortImpl* port, + Key key, + const Atom& value_atom); + + void pre_process(RunContext& ctx, Buffer* buffer); + void post_process(RunContext& ctx, Buffer* buffer); + + /** Get all bindings for `path` or children of `path`. */ + void get_all(const Raul::Path& path, std::vector<Binding*>& bindings); + + /** Remove a set of bindings from an earlier call to get_all(). */ + void remove(RunContext& ctx, const std::vector<Binding*>& bindings); + +private: + typedef boost::intrusive::multiset< + Binding, + boost::intrusive::compare<BindingLess> > Bindings; + + Key midi_event_key(uint16_t size, const uint8_t* buf, uint16_t& value); + + void set_port_value(RunContext& context, + PortImpl* port, + Type type, + int16_t value); + + bool finish_learn(RunContext& context, Key key); + + float control_to_port_value(RunContext& context, + const PortImpl* port, + Type type, + int16_t value) const; + + int16_t port_value_to_control(RunContext& context, + PortImpl* port, + Type type, + const Atom& value_atom) const; + + Engine& _engine; + std::atomic<Binding*> _learn_binding; + SPtr<Bindings> _bindings; + BufferRef _feedback; + LV2_Atom_Forge _forge; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_CONTROLBINDINGS_HPP diff --git a/src/server/DirectDriver.hpp b/src/server/DirectDriver.hpp new file mode 100644 index 00000000..58b4f898 --- /dev/null +++ b/src/server/DirectDriver.hpp @@ -0,0 +1,108 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_DIRECT_DRIVER_HPP +#define INGEN_ENGINE_DIRECT_DRIVER_HPP + +#include <boost/intrusive/slist.hpp> + +#include "Driver.hpp" +#include "Engine.hpp" + +namespace Ingen { +namespace Server { + +/** Driver for running Ingen directly as a library. + * \ingroup engine + */ +class DirectDriver : public Driver { +public: + DirectDriver(Engine& engine, + double sample_rate, + SampleCount block_length, + size_t seq_size) + : _engine(engine) + , _sample_rate(sample_rate) + , _block_length(block_length) + , _seq_size(seq_size) + {} + + virtual ~DirectDriver() { + _ports.clear_and_dispose([](EnginePort* p) { delete p; }); + } + + bool dynamic_ports() const { return true; } + + virtual EnginePort* create_port(DuplexPort* graph_port) { + return new EnginePort(graph_port); + } + + virtual EnginePort* get_port(const Raul::Path& path) { + for (auto& p : _ports) { + if (p.graph_port()->path() == path) { + return &p; + } + } + + return nullptr; + } + + virtual void add_port(RunContext& context, EnginePort* port) { + _ports.push_back(*port); + } + + virtual void remove_port(RunContext& context, EnginePort* port) { + _ports.erase(_ports.iterator_to(*port)); + } + + virtual void rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) {} + + virtual void port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) {} + + virtual void register_port(EnginePort& port) {} + virtual void unregister_port(EnginePort& port) {} + + virtual SampleCount block_length() const { return _block_length; } + + virtual size_t seq_size() const { return _seq_size; } + + virtual SampleCount sample_rate() const { return _sample_rate; } + + virtual SampleCount frame_time() const { return _engine.run_context().start(); } + + virtual void append_time_events(RunContext& context, Buffer& buffer) {} + + virtual int real_time_priority() { return 60; } + +private: + typedef boost::intrusive::slist<EnginePort, + boost::intrusive::cache_last<true> + > Ports; + + Engine& _engine; + Ports _ports; + SampleCount _sample_rate; + SampleCount _block_length; + size_t _seq_size; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_DIRECT_DRIVER_HPP diff --git a/src/server/Driver.hpp b/src/server/Driver.hpp new file mode 100644 index 00000000..9ae4b836 --- /dev/null +++ b/src/server/Driver.hpp @@ -0,0 +1,110 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_DRIVER_HPP +#define INGEN_ENGINE_DRIVER_HPP + +#include "raul/Noncopyable.hpp" + +#include "DuplexPort.hpp" +#include "EnginePort.hpp" + +namespace Raul { class Path; } + +namespace Ingen { +namespace Server { + +class DuplexPort; +class EnginePort; + +/** Engine driver base class. + * + * A Driver is responsible for managing system ports, and possibly running the + * audio graph. + * + * \ingroup engine + */ +class Driver : public Raul::Noncopyable { +public: + virtual ~Driver() = default; + + /** Activate driver (begin processing graph and events). */ + virtual bool activate() { return true; } + + /** Deactivate driver (stop processing graph and events). */ + virtual void deactivate() {} + + /** Create a port ready to be inserted with add_input (non realtime). + * May return NULL if the Driver can not create the port for some reason. + */ + virtual EnginePort* create_port(DuplexPort* graph_port) = 0; + + /** Find a system port by path. */ + virtual EnginePort* get_port(const Raul::Path& path) = 0; + + /** Add a system visible port (e.g. a port on the root graph). */ + virtual void add_port(RunContext& context, EnginePort* port) = 0; + + /** Remove a system visible port. + * + * This removes the port from the driver in the process thread but does not + * destroy the port. To actually remove the system port, unregister_port() + * must be called later in another thread. + */ + virtual void remove_port(RunContext& context, EnginePort* port) = 0; + + /** Return true iff driver supports dynamic adding/removing of ports. */ + virtual bool dynamic_ports() const { return false; } + + /** Register a system visible port. */ + virtual void register_port(EnginePort& port) = 0; + + /** Register a system visible port. */ + virtual void unregister_port(EnginePort& port) = 0; + + /** Rename a system visible port. */ + virtual void rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) = 0; + + /** Apply a system visible port property. */ + virtual void port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) = 0; + + /** Return the audio buffer size in frames */ + virtual SampleCount block_length() const = 0; + + /** Return the event buffer size in bytes */ + virtual size_t seq_size() const = 0; + + /** Return the sample rate in Hz */ + virtual SampleRate sample_rate() const = 0; + + /** Return the current frame time (running counter) */ + virtual SampleCount frame_time() const = 0; + + /** Append time events for this cycle to `buffer`. */ + virtual void append_time_events(RunContext& context, + Buffer& buffer) = 0; + + /** Return the real-time priority of the audio thread, or -1. */ + virtual int real_time_priority() = 0; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_DRIVER_HPP diff --git a/src/server/DuplexPort.cpp b/src/server/DuplexPort.cpp new file mode 100644 index 00000000..1b62ff38 --- /dev/null +++ b/src/server/DuplexPort.cpp @@ -0,0 +1,236 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" + +#include "Buffer.hpp" +#include "Driver.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" + +namespace Ingen { +namespace Server { + +DuplexPort::DuplexPort(BufferFactory& bufs, + GraphImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + bool polyphonic, + PortType type, + LV2_URID buf_type, + size_t buf_size, + const Atom& value, + bool is_output) + : InputPort(bufs, parent, symbol, index, parent->polyphony(), type, buf_type, value, buf_size) +{ + if (polyphonic) { + set_property(bufs.uris().ingen_polyphonic, bufs.forge().make(true)); + } + + if (!parent->parent() || + _poly != parent->parent_graph()->internal_poly()) { + _poly = 1; + } + + // Set default control range + if (!is_output && value.type() == bufs.uris().atom_Float) { + set_property(bufs.uris().lv2_minimum, bufs.forge().make(0.0f)); + set_property(bufs.uris().lv2_maximum, bufs.forge().make(1.0f)); + } + + _is_output = is_output; + if (is_output) { + if (parent->graph_type() != Node::GraphType::GRAPH) { + remove_property(bufs.uris().rdf_type, bufs.uris().lv2_InputPort.urid); + add_property(bufs.uris().rdf_type, bufs.uris().lv2_OutputPort.urid); + } + } + + get_buffers(bufs, &BufferFactory::get_buffer, + _voices, parent->polyphony(), 0); +} + +DuplexPort::~DuplexPort() +{ + if (is_linked()) { + parent_graph()->remove_port(*this); + } +} + +DuplexPort* +DuplexPort::duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) +{ + BufferFactory& bufs = *engine.buffer_factory(); + const Atom polyphonic = get_property(bufs.uris().ingen_polyphonic); + + DuplexPort* dup = new DuplexPort( + bufs, parent, symbol, _index, + polyphonic.type() == bufs.uris().atom_Bool && polyphonic.get<int32_t>(), + _type, _buffer_type, _buffer_size, + _value, _is_output); + + dup->set_properties(properties()); + + return dup; +} + +void +DuplexPort::inherit_neighbour(const PortImpl* port, + Properties& remove, + Properties& add) +{ + const URIs& uris = _bufs.uris(); + + /* TODO: This needs to become more sophisticated, and correct the situation + if the port is disconnected. */ + if (_type == PortType::CONTROL || _type == PortType::CV) { + if (port->minimum().get<float>() < _min.get<float>()) { + _min = port->minimum(); + remove.emplace(uris.lv2_minimum, uris.patch_wildcard); + add.emplace(uris.lv2_minimum, port->minimum()); + } + if (port->maximum().get<float>() > _max.get<float>()) { + _max = port->maximum(); + remove.emplace(uris.lv2_maximum, uris.patch_wildcard); + add.emplace(uris.lv2_maximum, port->maximum()); + } + } else if (_type == PortType::ATOM) { + for (auto i = port->properties().find(uris.atom_supports); + i != port->properties().end() && i->first == uris.atom_supports; + ++i) { + set_property(i->first, i->second); + add.insert(*i); + } + } +} + +void +DuplexPort::on_property(const URI& uri, const Atom& value) +{ + _bufs.engine().driver()->port_property(_path, uri, value); +} + +bool +DuplexPort::get_buffers(BufferFactory& bufs, + PortImpl::GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const +{ + if (!_is_driver_port && is_output()) { + return InputPort::get_buffers(bufs, get, voices, poly, num_in_arcs); + } else if (!_is_driver_port && is_input()) { + return PortImpl::get_buffers(bufs, get, voices, poly, num_in_arcs); + } + return false; +} + +bool +DuplexPort::setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly) +{ + if (!_is_driver_port && is_output()) { + return InputPort::setup_buffers(ctx, bufs, poly); + } else if (!_is_driver_port && is_input()) { + return PortImpl::setup_buffers(ctx, bufs, poly); + } + return false; +} + +void +DuplexPort::set_is_driver_port(BufferFactory& bufs) +{ + _voices->at(0).buffer = new Buffer(bufs, buffer_type(), _value.type(), 0, true, nullptr); + PortImpl::set_is_driver_port(bufs); +} + +void +DuplexPort::set_driver_buffer(void* buf, uint32_t capacity) +{ + _voices->at(0).buffer->set_buffer(buf); + _voices->at(0).buffer->set_capacity(capacity); +} + +uint32_t +DuplexPort::max_tail_poly(RunContext& context) const +{ + return std::max(_poly, parent_graph()->internal_poly_process()); +} + +bool +DuplexPort::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + if (!parent()->parent() || + poly != parent()->parent_graph()->internal_poly()) { + return false; + } + + return PortImpl::prepare_poly(bufs, poly); +} + +bool +DuplexPort::apply_poly(RunContext& context, uint32_t poly) +{ + if (!parent()->parent() || + poly != parent()->parent_graph()->internal_poly()) { + return false; + } + + return PortImpl::apply_poly(context, poly); +} + +void +DuplexPort::pre_process(RunContext& context) +{ + if (_is_output) { + /* This is a graph output, which is an input from the internal + perspective. Prepare buffers for write so plugins can deliver to + them */ + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer->prepare_write(context); + } + } else { + /* This is a a graph input, which is an output from the internal + perspective. Do whatever a normal block's input port does to + prepare input for reading. */ + InputPort::pre_process(context); + InputPort::pre_run(context); + } +} + +void +DuplexPort::post_process(RunContext& context) +{ + if (_is_output) { + /* This is a graph output, which is an input from the internal + perspective. Mix down input delivered by plugins so output + (external perspective) is ready. */ + InputPort::pre_process(context); + InputPort::pre_run(context); + } + monitor(context); +} + +SampleCount +DuplexPort::next_value_offset(SampleCount offset, SampleCount end) const +{ + return PortImpl::next_value_offset(offset, end); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/DuplexPort.hpp b/src/server/DuplexPort.hpp new file mode 100644 index 00000000..b0066164 --- /dev/null +++ b/src/server/DuplexPort.hpp @@ -0,0 +1,98 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_DUPLEXPORT_HPP +#define INGEN_ENGINE_DUPLEXPORT_HPP + +#include <boost/intrusive/slist.hpp> + +#include "BufferRef.hpp" +#include "InputPort.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; + +/** A duplex Port (both an input and output port on a Graph) + * + * This is used for Graph ports, since they need to appear as both an input and + * an output port based on context. There are no actual duplex ports in Ingen, + * a Port is either an Input or Output. This class only exists to allow Graph + * outputs to appear as inputs from within that Graph, and vice versa. + * + * \ingroup engine + */ +class DuplexPort : public InputPort + , public boost::intrusive::slist_base_hook<> // In GraphImpl +{ +public: + DuplexPort(BufferFactory& bufs, + GraphImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + bool polyphonic, + PortType type, + LV2_URID buf_type, + size_t buf_size, + const Atom& value, + bool is_output); + + virtual ~DuplexPort(); + + DuplexPort* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent); + + void inherit_neighbour(const PortImpl* port, + Properties& remove, + Properties& add); + + void on_property(const URI& uri, const Atom& value); + + uint32_t max_tail_poly(RunContext& context) const; + + bool prepare_poly(BufferFactory& bufs, uint32_t poly); + + bool apply_poly(RunContext& context, uint32_t poly); + + bool get_buffers(BufferFactory& bufs, + PortImpl::GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const; + + virtual void set_is_driver_port(BufferFactory& bufs); + + /** Set the external driver-provided buffer. + * + * This may only be called in the process thread, after an earlier call to + * prepare_driver_buffer(). + */ + void set_driver_buffer(void* buf, uint32_t capacity); + + bool setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly); + + void pre_process(RunContext& context); + void post_process(RunContext& context); + + SampleCount next_value_offset(SampleCount offset, SampleCount end) const; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_DUPLEXPORT_HPP diff --git a/src/server/Engine.cpp b/src/server/Engine.cpp new file mode 100644 index 00000000..a7476845 --- /dev/null +++ b/src/server/Engine.cpp @@ -0,0 +1,526 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen_config.h" + +#include <sys/mman.h> + +#include <limits> +#include <thread> + +#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" + +#include "events/CreateGraph.hpp" +#include "ingen/AtomReader.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/Store.hpp" +#include "ingen/StreamWriter.hpp" +#include "ingen/Tee.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/types.hpp" +#include "raul/Maid.hpp" + +#include "BlockFactory.hpp" +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "ControlBindings.hpp" +#include "DirectDriver.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "Event.hpp" +#include "EventWriter.hpp" +#include "GraphImpl.hpp" +#include "LV2Options.hpp" +#include "PostProcessor.hpp" +#include "PreProcessContext.hpp" +#include "PreProcessor.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" +#include "UndoStack.hpp" +#include "Worker.hpp" +#ifdef HAVE_SOCKET +#include "SocketListener.hpp" +#endif + +namespace Ingen { +namespace Server { + +INGEN_THREAD_LOCAL unsigned ThreadManager::flags(0); +bool ThreadManager::single_threaded(true); + +Engine::Engine(Ingen::World* world) + : _world(world) + , _options(new LV2Options(world->uris())) + , _buffer_factory(new BufferFactory(*this, world->uris())) + , _maid(new Raul::Maid) + , _worker(new Worker(world->log(), event_queue_size())) + , _sync_worker(new Worker(world->log(), event_queue_size(), true)) + , _broadcaster(new Broadcaster()) + , _control_bindings(new ControlBindings(*this)) + , _block_factory(new BlockFactory(world)) + , _undo_stack(new UndoStack(_world->uris(), _world->uri_map())) + , _redo_stack(new UndoStack(_world->uris(), _world->uri_map())) + , _post_processor(new PostProcessor(*this)) + , _pre_processor(new PreProcessor(*this)) + , _event_writer(new EventWriter(*this)) + , _interface(_event_writer) + , _atom_interface( + new AtomReader(world->uri_map(), world->uris(), world->log(), *_interface)) + , _root_graph(nullptr) + , _cycle_start_time(0) + , _rand_engine(0) + , _uniform_dist(0.0f, 1.0f) + , _quit_flag(false) + , _reset_load_flag(false) + , _atomic_bundles(world->conf().option("atomic-bundles").get<int32_t>()) + , _activated(false) +{ + if (!world->store()) { + world->set_store(SPtr<Ingen::Store>(new Store())); + } + + for (int i = 0; i < world->conf().option("threads").get<int32_t>(); ++i) { + Raul::RingBuffer* ring = new Raul::RingBuffer(24 * event_queue_size()); + _notifications.push_back(ring); + _run_contexts.push_back(new RunContext(*this, ring, i, i > 0)); + } + + _world->lv2_features().add_feature(_worker->schedule_feature()); + _world->lv2_features().add_feature(_options); + _world->lv2_features().add_feature( + SPtr<LV2Features::Feature>( + new LV2Features::EmptyFeature(LV2_BUF_SIZE__powerOf2BlockLength))); + _world->lv2_features().add_feature( + SPtr<LV2Features::Feature>( + new LV2Features::EmptyFeature(LV2_BUF_SIZE__fixedBlockLength))); + _world->lv2_features().add_feature( + SPtr<LV2Features::Feature>( + new LV2Features::EmptyFeature(LV2_BUF_SIZE__boundedBlockLength))); + _world->lv2_features().add_feature( + SPtr<LV2Features::Feature>( + new LV2Features::EmptyFeature(LV2_STATE__loadDefaultState))); + + if (world->conf().option("dump").get<int32_t>()) { + _interface = std::make_shared<Tee>( + Tee::Sinks{ + _event_writer, + std::make_shared<StreamWriter>(world->uri_map(), + world->uris(), + URI("ingen:/engine"), + stderr, + ColorContext::Color::MAGENTA)}); + } +} + +Engine::~Engine() +{ + _root_graph = nullptr; + deactivate(); + + // Process all pending events + const FrameTime end = std::numeric_limits<FrameTime>::max(); + RunContext& ctx = run_context(); + locate(ctx.end(), end - ctx.end()); + _post_processor->set_end_time(end); + _post_processor->process(); + while (!_pre_processor->empty()) { + _pre_processor->process(ctx, *_post_processor, 1); + _post_processor->process(); + } + + _atom_interface.reset(); + + // Delete run contexts + _quit_flag = true; + _tasks_available.notify_all(); + for (RunContext* ctx : _run_contexts) { + ctx->join(); + delete ctx; + } + for (Raul::RingBuffer* ring : _notifications) { + delete ring; + } + + const SPtr<Store> store = this->store(); + if (store) { + for (auto& s : *store.get()) { + if (!dynamic_ptr_cast<NodeImpl>(s.second)->parent()) { + s.second.reset(); + } + } + store->clear(); + } + + _world->set_store(SPtr<Ingen::Store>()); +} + +void +Engine::listen() +{ +#ifdef HAVE_SOCKET + _listener = UPtr<SocketListener>(new SocketListener(*this)); +#endif +} + +void +Engine::advance(SampleCount nframes) +{ + for (RunContext* ctx : _run_contexts) { + ctx->locate(ctx->start() + nframes, block_length()); + } +} + +void +Engine::locate(FrameTime s, SampleCount nframes) +{ + for (RunContext* ctx : _run_contexts) { + ctx->locate(s, nframes); + } +} + +void +Engine::set_root_graph(GraphImpl* graph) +{ + _root_graph = graph; +} + +void +Engine::flush_events(const std::chrono::milliseconds& sleep_ms) +{ + bool finished = !pending_events(); + while (!finished) { + // Run one audio block to execute prepared events + run(block_length()); + advance(block_length()); + + // Run one main iteration to post-process events + main_iteration(); + + // Sleep before continuing if there are still events to process + if (!(finished = !pending_events())) { + std::this_thread::sleep_for(sleep_ms); + } + } +} + +void +Engine::emit_notifications(FrameTime end) +{ + for (RunContext* ctx : _run_contexts) { + ctx->emit_notifications(end); + } +} + +bool +Engine::pending_notifications() +{ + for (const RunContext* ctx : _run_contexts) { + if (ctx->pending_notifications()) { + return true; + } + } + return false; +} + +bool +Engine::wait_for_tasks() +{ + if (!_quit_flag) { + std::unique_lock<std::mutex> lock(_tasks_mutex); + _tasks_available.wait(lock); + } + return !_quit_flag; +} + +void +Engine::signal_tasks_available() +{ + _tasks_available.notify_all(); +} + +Task* +Engine::steal_task(unsigned start_thread) +{ + for (unsigned i = 0; i < _run_contexts.size(); ++i) { + const unsigned id = (start_thread + i) % _run_contexts.size(); + RunContext* const ctx = _run_contexts[id]; + Task* par = ctx->task(); + if (par) { + Task* t = par->steal(*ctx); + if (t) { + return t; + } + } + } + return nullptr; +} + +SPtr<Store> +Engine::store() const +{ + return _world->store(); +} + +SampleRate +Engine::sample_rate() const +{ + return _driver->sample_rate(); +} + +SampleCount +Engine::block_length() const +{ + return _driver->block_length(); +} + +size_t +Engine::sequence_size() const +{ + return _driver->seq_size(); +} + +size_t +Engine::event_queue_size() const +{ + return world()->conf().option("queue-size").get<int32_t>(); +} + +void +Engine::quit() +{ + _quit_flag = true; +} + +Properties +Engine::load_properties() const +{ + const Ingen::URIs& uris = world()->uris(); + + return { { uris.ingen_meanRunLoad, + uris.forge.make(floorf(_run_load.mean) / 100.0f) }, + { uris.ingen_minRunLoad, + uris.forge.make(_run_load.min / 100.0f) }, + { uris.ingen_maxRunLoad, + uris.forge.make(_run_load.max / 100.0f) } }; +} + +bool +Engine::main_iteration() +{ + _post_processor->process(); + _maid->cleanup(); + + if (_run_load.changed) { + _broadcaster->put(URI("ingen:/engine"), load_properties()); + _run_load.changed = false; + } + + return !_quit_flag; +} + +void +Engine::set_driver(SPtr<Driver> driver) +{ + _driver = driver; + for (RunContext* ctx : _run_contexts) { + ctx->set_priority(driver->real_time_priority()); + ctx->set_rate(driver->sample_rate()); + } + + _buffer_factory->set_block_length(driver->block_length()); + _options->set(sample_rate(), + block_length(), + buffer_factory()->default_size(_world->uris().atom_Sequence)); +} + +SampleCount +Engine::event_time() +{ + if (ThreadManager::single_threaded) { + return 0; + } + + return _driver->frame_time() + _driver->block_length(); +} + +uint64_t +Engine::current_time() const +{ + return _clock.now_microseconds(); +} + +void +Engine::reset_load() +{ + _reset_load_flag = true; +} + +void +Engine::init(double sample_rate, uint32_t block_length, size_t seq_size) +{ + set_driver(SPtr<Driver>(new DirectDriver(*this, sample_rate, block_length, seq_size))); +} + +bool +Engine::supports_dynamic_ports() const +{ + return !_driver || _driver->dynamic_ports(); +} + +bool +Engine::activate() +{ + if (!_driver) { + return false; + } + + ThreadManager::single_threaded = true; + + const Ingen::URIs& uris = world()->uris(); + + if (!_root_graph) { + // No root graph has been loaded, create an empty one + const Properties properties = { + {uris.rdf_type, uris.ingen_Graph}, + {uris.ingen_polyphony, + Property(_world->forge().make(1), + Resource::Graph::INTERNAL)}}; + + enqueue_event( + new Events::CreateGraph( + *this, SPtr<Interface>(), -1, 0, Raul::Path("/"), properties)); + + flush_events(std::chrono::milliseconds(10)); + if (!_root_graph) { + return false; + } + } + + _driver->activate(); + _root_graph->enable(); + + ThreadManager::single_threaded = false; + _activated = true; + + return true; +} + +void +Engine::deactivate() +{ + if (_driver) { + _driver->deactivate(); + } + + if (_root_graph) { + _root_graph->deactivate(); + } + + ThreadManager::single_threaded = true; + _activated = false; +} + +unsigned +Engine::run(uint32_t sample_count) +{ + RunContext& ctx = run_context(); + _cycle_start_time = current_time(); + + post_processor()->set_end_time(ctx.end()); + + // Process events that came in during the last cycle + // (Aiming for jitter-free 1 block event latency, ideally) + const unsigned n_processed_events = process_events(); + + // Reset load if graph structure has changed + if (_reset_load_flag) { + _run_load = Load(); + _reset_load_flag = false; + } + + // Run root graph + if (_root_graph) { + // Apply control bindings to input + control_bindings()->pre_process( + ctx, _root_graph->port_impl(0)->buffer(0).get()); + + // Run root graph for this cycle + _root_graph->process(ctx); + + // Emit control binding feedback + control_bindings()->post_process( + ctx, _root_graph->port_impl(1)->buffer(0).get()); + } + + // Update load for this cycle + if (ctx.duration() > 0) { + _run_load.update(current_time() - _cycle_start_time, ctx.duration()); + } + + return n_processed_events; +} + +bool +Engine::pending_events() const +{ + return !_pre_processor->empty() || _post_processor->pending(); +} + +void +Engine::enqueue_event(Event* ev, Event::Mode mode) +{ + _pre_processor->event(ev, mode); +} + +unsigned +Engine::process_events() +{ + const size_t MAX_EVENTS_PER_CYCLE = run_context().nframes() / 8; + return _pre_processor->process( + run_context(), *_post_processor, MAX_EVENTS_PER_CYCLE); +} + +unsigned +Engine::process_all_events() +{ + return _pre_processor->process(run_context(), *_post_processor, 0); +} + +Log& +Engine::log() const +{ + return _world->log(); +} + +void +Engine::register_client(SPtr<Interface> client) +{ + log().info(fmt("Registering client <%1%>\n") % client->uri().c_str()); + _broadcaster->register_client(client); +} + +bool +Engine::unregister_client(SPtr<Interface> client) +{ + log().info(fmt("Unregistering client <%1%>\n") % client->uri().c_str()); + return _broadcaster->unregister_client(client); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Engine.hpp b/src/server/Engine.hpp new file mode 100644 index 00000000..f5ba1feb --- /dev/null +++ b/src/server/Engine.hpp @@ -0,0 +1,221 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_ENGINE_HPP +#define INGEN_ENGINE_ENGINE_HPP + +#include <chrono> +#include <condition_variable> +#include <mutex> +#include <random> + +#include "ingen/Clock.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Properties.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" + +#include "Event.hpp" +#include "Load.hpp" + +namespace Raul { +class Maid; +class RingBuffer; +} + +namespace Ingen { + +class AtomReader; +class Interface; +class Log; +class Store; +class World; + +namespace Server { + +class BlockFactory; +class Broadcaster; +class BufferFactory; +class ControlBindings; +class Driver; +class EventWriter; +class GraphImpl; +class LV2Options; +class PostProcessor; +class PreProcessor; +class RunContext; +class SocketListener; +class Task; +class UndoStack; +class Worker; + +/** + The engine which executes the process graph. + + This is a simple class that provides pointers to the various components + that make up the engine implementation. In processes with a local engine, + it can be accessed via the Ingen::World. + + @ingroup engine +*/ +class INGEN_API Engine : public EngineBase +{ +public: + explicit Engine(Ingen::World* world); + virtual ~Engine(); + + Engine(const Engine&) = delete; + Engine& operator=(const Engine&) = delete; + + // EngineBase methods + virtual void init(double sample_rate, uint32_t block_length, size_t seq_size); + virtual bool supports_dynamic_ports() const; + virtual bool activate(); + virtual void deactivate(); + virtual bool pending_events() const; + virtual unsigned run(uint32_t sample_count); + virtual void quit(); + virtual bool main_iteration(); + virtual void register_client(SPtr<Interface> client); + virtual bool unregister_client(SPtr<Interface> client); + + void listen(); + + /** Return a random [0..1] float with uniform distribution */ + float frand() { return _uniform_dist(_rand_engine); } + + void set_driver(SPtr<Driver> driver); + + /** Return the frame time to execute an event that arrived now. + * + * This aims to return a time one cycle from "now", so that events ideally + * have 1 cycle of latency with no jitter. + */ + SampleCount event_time(); + + /** Return the time this cycle began processing in microseconds. + * + * This value is comparable to the value returned by current_time(). + */ + inline uint64_t cycle_start_time(const RunContext& context) const { + return _cycle_start_time; + } + + /** Return the current time in microseconds. */ + uint64_t current_time() const; + + /** Reset the load statistics (when the expected DSP load changes). */ + void reset_load(); + + /** Enqueue an event to be processed (non-realtime threads only). */ + void enqueue_event(Event* ev, Event::Mode mode=Event::Mode::NORMAL); + + /** Process events (process thread only). */ + unsigned process_events(); + + /** Process all events (no RT limits). */ + unsigned process_all_events(); + + Ingen::World* world() const { return _world; } + Log& log() const; + + const SPtr<Interface>& interface() const { return _interface; } + const SPtr<EventWriter>& event_writer() const { return _event_writer; } + const UPtr<AtomReader>& atom_interface() const { return _atom_interface; } + const UPtr<BlockFactory>& block_factory() const { return _block_factory; } + const UPtr<Broadcaster>& broadcaster() const { return _broadcaster; } + const UPtr<BufferFactory>& buffer_factory() const { return _buffer_factory; } + const UPtr<ControlBindings>& control_bindings() const { return _control_bindings; } + const SPtr<Driver>& driver() const { return _driver; } + const UPtr<PostProcessor>& post_processor() const { return _post_processor; } + const UPtr<Raul::Maid>& maid() const { return _maid; } + const UPtr<UndoStack>& undo_stack() const { return _undo_stack; } + const UPtr<UndoStack>& redo_stack() const { return _redo_stack; } + const UPtr<Worker>& worker() const { return _worker; } + const UPtr<Worker>& sync_worker() const { return _sync_worker; } + + GraphImpl* root_graph() const { return _root_graph; } + void set_root_graph(GraphImpl* graph); + + RunContext& run_context() { return *_run_contexts[0]; } + + void flush_events(const std::chrono::milliseconds& sleep_ms); + + void advance(SampleCount nframes); + void locate(FrameTime s, SampleCount nframes); + void emit_notifications(FrameTime end); + bool pending_notifications(); + bool wait_for_tasks(); + void signal_tasks_available(); + Task* steal_task(unsigned start_thread); + + SPtr<Store> store() const; + + SampleRate sample_rate() const; + SampleCount block_length() const; + size_t sequence_size() const; + size_t event_queue_size() const; + + size_t n_threads() const { return _run_contexts.size(); } + bool atomic_bundles() const { return _atomic_bundles; } + bool activated() const { return _activated; } + + Properties load_properties() const; + +private: + Ingen::World* _world; + + SPtr<LV2Options> _options; + UPtr<BufferFactory> _buffer_factory; + UPtr<Raul::Maid> _maid; + SPtr<Driver> _driver; + UPtr<Worker> _worker; + UPtr<Worker> _sync_worker; + UPtr<Broadcaster> _broadcaster; + UPtr<ControlBindings> _control_bindings; + UPtr<BlockFactory> _block_factory; + UPtr<UndoStack> _undo_stack; + UPtr<UndoStack> _redo_stack; + UPtr<PostProcessor> _post_processor; + UPtr<PreProcessor> _pre_processor; + UPtr<SocketListener> _listener; + SPtr<EventWriter> _event_writer; + SPtr<Interface> _interface; + UPtr<AtomReader> _atom_interface; + GraphImpl* _root_graph; + + std::vector<Raul::RingBuffer*> _notifications; + std::vector<RunContext*> _run_contexts; + uint64_t _cycle_start_time; + Load _run_load; + Clock _clock; + + std::mt19937 _rand_engine; + std::uniform_real_distribution<float> _uniform_dist; + + std::condition_variable _tasks_available; + std::mutex _tasks_mutex; + + bool _quit_flag; + bool _reset_load_flag; + bool _atomic_bundles; + bool _activated; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_ENGINE_HPP diff --git a/src/server/EnginePort.hpp b/src/server/EnginePort.hpp new file mode 100644 index 00000000..c14f363c --- /dev/null +++ b/src/server/EnginePort.hpp @@ -0,0 +1,66 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_ENGINE_PORT_HPP +#define INGEN_ENGINE_ENGINE_PORT_HPP + +#include "raul/Deletable.hpp" +#include "raul/Noncopyable.hpp" + +#include <boost/intrusive/slist.hpp> + +#include "DuplexPort.hpp" + +namespace Ingen { +namespace Server { + +/** A "system" port (e.g. a Jack port, an external port on Ingen). + * + * @ingroup engine + */ +class EnginePort : public Raul::Noncopyable + , public Raul::Deletable + , public boost::intrusive::slist_base_hook<> +{ +public: + explicit EnginePort(DuplexPort* port) + : _graph_port(port) + , _buffer(nullptr) + , _handle(nullptr) + , _driver_index(0) + {} + + void set_buffer(void* buf) { _buffer = buf; } + void set_handle(void* buf) { _handle = buf; } + void set_driver_index(uint32_t index) { _driver_index = index; } + + void* buffer() const { return _buffer; } + void* handle() const { return _handle; } + uint32_t driver_index() const { return _driver_index; } + DuplexPort* graph_port() const { return _graph_port; } + bool is_input() const { return _graph_port->is_input(); } + +protected: + DuplexPort* _graph_port; + void* _buffer; + void* _handle; + uint32_t _driver_index; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_ENGINE_PORT_HPP diff --git a/src/server/Event.hpp b/src/server/Event.hpp new file mode 100644 index 00000000..d9095def --- /dev/null +++ b/src/server/Event.hpp @@ -0,0 +1,163 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_EVENT_HPP +#define INGEN_ENGINE_EVENT_HPP + +#include <atomic> + +#include "raul/Deletable.hpp" +#include "raul/Noncopyable.hpp" +#include "raul/Path.hpp" + +#include "ingen/Interface.hpp" +#include "ingen/Node.hpp" +#include "ingen/Status.hpp" +#include "ingen/types.hpp" + +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class RunContext; +class PreProcessContext; + +/** An event (command) to perform some action on Ingen. + * + * Virtually all operations on Ingen are implemented as events. An event has + * three distinct execution phases: + * + * 1) Pre-process: In a non-realtime thread, prepare event for execution + * 2) Execute: In the audio thread, execute (apply) event + * 3) Post-process: In a non-realtime thread, finalize event + * (e.g. clean up and send replies) + * + * \ingroup engine + */ +class Event : public Raul::Deletable, public Raul::Noncopyable +{ +public: + /** Event mode to distinguish normal events from undo events. */ + enum class Mode { NORMAL, UNDO, REDO }; + + /** Execution mode for events that block and unblock preprocessing. */ + enum class Execution { + NORMAL, ///< Normal pipelined execution + ATOMIC, ///< Block pre-processing until this event is executed + BLOCK, ///< Begin atomic block of events + UNBLOCK ///< Finish atomic executed block of events + }; + + /** Pre-process event before execution (non-realtime). */ + virtual bool pre_process(PreProcessContext& ctx) = 0; + + /** Execute this event in the audio thread (realtime). */ + virtual void execute(RunContext& context) = 0; + + /** Post-process event after execution (non-realtime). */ + virtual void post_process() = 0; + + /** Write the inverse of this event to `sink`. */ + virtual void undo(Interface& target) {} + + /** Return true iff this event has been pre-processed. */ + inline bool is_prepared() const { return _status != Status::NOT_PREPARED; } + + /** Return the time stamp of this event. */ + inline SampleCount time() const { return _time; } + + /** Set the time stamp of this event. */ + inline void set_time(SampleCount time) { _time = time; } + + /** Get the next event to be processed after this one. */ + Event* next() const { return _next.load(); } + + /** Set the next event to be processed after this one. */ + void next(Event* ev) { _next = ev; } + + /** Return the status (success or error code) of this event. */ + Status status() const { return _status; } + + /** Return the blocking behaviour of this event (after construction). */ + virtual Execution get_execution() const { return Execution::NORMAL; } + + /** Return undo mode of this event. */ + Mode get_mode() const { return _mode; } + + /** Set the undo mode of this event. */ + void set_mode(Mode mode) { _mode = mode; } + + inline Engine& engine() { return _engine; } + +protected: + Event(Engine& engine, SPtr<Interface> client, int32_t id, FrameTime time) + : _engine(engine) + , _next(nullptr) + , _request_client(std::move(client)) + , _request_id(id) + , _time(time) + , _status(Status::NOT_PREPARED) + , _mode(Mode::NORMAL) + {} + + /** Constructor for internal events only */ + explicit Event(Engine& engine) + : _engine(engine) + , _next(nullptr) + , _request_id(0) + , _time(0) + , _status(Status::NOT_PREPARED) + , _mode(Mode::NORMAL) + {} + + inline bool pre_process_done(Status st) { + _status = st; + return st == Status::SUCCESS; + } + + inline bool pre_process_done(Status st, const URI& subject) { + _err_subject = subject; + return pre_process_done(st); + } + + inline bool pre_process_done(Status st, const Raul::Path& subject) { + return pre_process_done(st, path_to_uri(subject)); + } + + /** Respond to the originating client. */ + inline Status respond() { + if (_request_client && _request_id) { + _request_client->response(_request_id, _status, _err_subject); + } + return _status; + } + + Engine& _engine; + std::atomic<Event*> _next; + SPtr<Interface> _request_client; + int32_t _request_id; + FrameTime _time; + Status _status; + std::string _err_subject; + Mode _mode; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_EVENT_HPP diff --git a/src/server/EventWriter.cpp b/src/server/EventWriter.cpp new file mode 100644 index 00000000..ebdf7562 --- /dev/null +++ b/src/server/EventWriter.cpp @@ -0,0 +1,147 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <boost/variant/apply_visitor.hpp> + +#include "ingen/URIs.hpp" + +#include "Engine.hpp" +#include "EventWriter.hpp" +#include "events.hpp" + +namespace Ingen { +namespace Server { + +EventWriter::EventWriter(Engine& engine) + : _engine(engine) + , _event_mode(Event::Mode::NORMAL) +{ +} + +SampleCount +EventWriter::now() const +{ + return _engine.event_time(); +} + +void +EventWriter::message(const Message& msg) +{ + boost::apply_visitor(*this, msg); +} + +void +EventWriter::operator()(const BundleBegin& msg) +{ + _engine.enqueue_event(new Events::Mark(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const BundleEnd& msg) +{ + _engine.enqueue_event(new Events::Mark(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Put& msg) +{ + _engine.enqueue_event(new Events::Delta(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Delta& msg) +{ + _engine.enqueue_event(new Events::Delta(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Copy& msg) +{ + _engine.enqueue_event(new Events::Copy(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Move& msg) +{ + _engine.enqueue_event(new Events::Move(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Del& msg) +{ + _engine.enqueue_event(new Events::Delete(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Connect& msg) +{ + _engine.enqueue_event(new Events::Connect(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Disconnect& msg) +{ + _engine.enqueue_event( + new Events::Disconnect(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const DisconnectAll& msg) +{ + _engine.enqueue_event( + new Events::DisconnectAll(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const SetProperty& msg) +{ + _engine.enqueue_event(new Events::Delta(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Undo& msg) +{ + _engine.enqueue_event(new Events::Undo(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Redo& msg) +{ + _engine.enqueue_event(new Events::Undo(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Get& msg) +{ + _engine.enqueue_event(new Events::Get(_engine, _respondee, now(), msg), + _event_mode); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/EventWriter.hpp b/src/server/EventWriter.hpp new file mode 100644 index 00000000..2d4b9724 --- /dev/null +++ b/src/server/EventWriter.hpp @@ -0,0 +1,86 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_EVENTWRITER_HPP +#define INGEN_ENGINE_EVENTWRITER_HPP + +#include <memory> +#include <string> + +#include "ingen/Interface.hpp" +#include "ingen/Resource.hpp" +#include "ingen/types.hpp" + +#include "Event.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class Engine; + +/** An Interface that creates and enqueues Events for the Engine to execute. + */ +class EventWriter : public Interface +{ +public: + explicit EventWriter(Engine& engine); + + URI uri() const override { return URI("ingen:/clients/event_writer"); } + + SPtr<Interface> respondee() const override { + return _respondee; + } + + void set_respondee(SPtr<Interface> respondee) override { + _respondee = respondee; + } + + void message(const Message& msg) override; + + void set_event_mode(Event::Mode mode) { _event_mode = mode; } + Event::Mode get_event_mode() { return _event_mode; } + + void operator()(const BundleBegin&); + void operator()(const BundleEnd&); + void operator()(const Connect&); + void operator()(const Copy&); + void operator()(const Del&); + void operator()(const Delta&); + void operator()(const Disconnect&); + void operator()(const DisconnectAll&); + void operator()(const Error&) {} + void operator()(const Get&); + void operator()(const Move&); + void operator()(const Put&); + void operator()(const Redo&); + void operator()(const Response&) {} + void operator()(const SetProperty&); + void operator()(const Undo&); + +protected: + Engine& _engine; + SPtr<Interface> _respondee; + Event::Mode _event_mode; + +private: + SampleCount now() const; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_EVENTWRITER_HPP diff --git a/src/server/FrameTimer.hpp b/src/server/FrameTimer.hpp new file mode 100644 index 00000000..367ac900 --- /dev/null +++ b/src/server/FrameTimer.hpp @@ -0,0 +1,110 @@ +/* + This file is part of Ingen. + Copyright 2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_FRAMETIMER_HPP +#define INGEN_ENGINE_FRAMETIMER_HPP + +#include <chrono> +#include <cmath> +#include <cstdint> + +namespace Ingen { +namespace Server { + +/** Delay-locked loop for monotonic sample time. + * + * See "Using a DLL to filter time" by Fons Adriaensen + * http://kokkinizita.linuxaudio.org/papers/usingdll.pdf + */ +class FrameTimer +{ +public: + static constexpr double PI = 3.14159265358979323846; + static constexpr double bandwidth = 1.0 / 8.0; // Hz + static constexpr double us_per_s = 1000000.0; + + FrameTimer(uint32_t period_size, uint32_t sample_rate) + : tper(((double)period_size / (double)sample_rate) * us_per_s) + , omega(2 * PI * bandwidth / us_per_s * tper) + , b(sqrt(2) * omega) + , c(omega * omega) + , nper(period_size) + { + } + + /** Update the timer for current real time `usec` and frame `frame`. */ + void update(uint64_t usec, uint64_t frame) { + if (!initialized || frame != n1) { + init(usec, frame); + return; + } + + // Calculate loop error + const double e = ((double)usec - t1); + + // Update loop + t0 = t1; + t1 += b * e + e2; + e2 += c * e; + + // Update frame counts + n0 = n1; + n1 += nper; + } + + /** Return an estimate of the frame time for current real time `usec`. */ + uint64_t frame_time(uint64_t usec) const { + if (!initialized) { + return 0; + } + + const double delta = (double)usec - t0; + const double period = t1 - t0; + return n0 + std::round(delta / period * nper); + } + +private: + void init(uint64_t now, uint64_t frame) { + // Init loop + e2 = tper; + t0 = now; + t1 = t0 + e2; + + // Init sample counts + n0 = frame; + n1 = n0 + nper; + + initialized = true; + } + + const double tper; + const double omega; + const double b; + const double c; + + uint64_t nper; + double e2; + double t0; + double t1; + uint64_t n0; + uint64_t n1; + bool initialized; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_FRAMETIMER_HPP diff --git a/src/server/GraphImpl.cpp b/src/server/GraphImpl.cpp new file mode 100644 index 00000000..f9c4cb54 --- /dev/null +++ b/src/server/GraphImpl.cpp @@ -0,0 +1,379 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <unordered_map> + +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "raul/Maid.hpp" + +#include "ArcImpl.hpp" +#include "BlockImpl.hpp" +#include "BufferFactory.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "GraphPlugin.hpp" +#include "PortImpl.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +GraphImpl::GraphImpl(Engine& engine, + const Raul::Symbol& symbol, + uint32_t poly, + GraphImpl* parent, + SampleRate srate, + uint32_t internal_poly) + : BlockImpl(new GraphPlugin(engine.world()->uris(), + engine.world()->uris().ingen_Graph, + Raul::Symbol("graph"), + "Ingen Graph"), + symbol, poly, parent, srate) + , _engine(engine) + , _poly_pre(internal_poly) + , _poly_process(internal_poly) + , _process(false) +{ + assert(internal_poly >= 1); + assert(internal_poly <= 128); +} + +GraphImpl::~GraphImpl() +{ + delete _plugin; +} + +BlockImpl* +GraphImpl::duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) +{ + BufferFactory& bufs = *engine.buffer_factory(); + const SampleRate rate = engine.sample_rate(); + + // Duplicate graph + GraphImpl* dup = new GraphImpl( + engine, symbol, _polyphony, parent, rate, _poly_process); + + Properties props = properties(); + props.erase(bufs.uris().lv2_symbol); + props.insert({bufs.uris().lv2_symbol, bufs.forge().alloc(symbol.c_str())}); + dup->set_properties(props); + + // We need a map of port duplicates to duplicate arcs + typedef std::unordered_map<PortImpl*, PortImpl*> PortMap; + PortMap port_map; + + // Add duplicates of all ports + dup->_ports = bufs.maid().make_managed<Ports>(num_ports(), nullptr); + for (PortList::iterator p = _inputs.begin(); p != _inputs.end(); ++p) { + DuplexPort* p_dup = p->duplicate(engine, p->symbol(), dup); + dup->_inputs.push_front(*p_dup); + (*dup->_ports)[p->index()] = p_dup; + port_map.insert({&*p, p_dup}); + } + for (PortList::iterator p = _outputs.begin(); p != _outputs.end(); ++p) { + DuplexPort* p_dup = p->duplicate(engine, p->symbol(), dup); + dup->_outputs.push_front(*p_dup); + (*dup->_ports)[p->index()] = p_dup; + port_map.insert({&*p, p_dup}); + } + + // Add duplicates of all blocks + for (auto& b : _blocks) { + BlockImpl* b_dup = b.duplicate(engine, b.symbol(), dup); + dup->add_block(*b_dup); + b_dup->activate(*engine.buffer_factory()); + for (uint32_t p = 0; p < b.num_ports(); ++p) { + port_map.insert({b.port_impl(p), b_dup->port_impl(p)}); + } + } + + // Add duplicates of all arcs + for (const auto& a : _arcs) { + SPtr<ArcImpl> arc = dynamic_ptr_cast<ArcImpl>(a.second); + if (arc) { + auto t = port_map.find(arc->tail()); + auto h = port_map.find(arc->head()); + if (t != port_map.end() && h != port_map.end()) { + dup->add_arc(SPtr<ArcImpl>(new ArcImpl(t->second, h->second))); + } + } + } + + return dup; +} + +void +GraphImpl::activate(BufferFactory& bufs) +{ + BlockImpl::activate(bufs); + + for (auto& b : _blocks) { + b.activate(bufs); + } + + assert(_activated); +} + +void +GraphImpl::deactivate() +{ + if (_activated) { + BlockImpl::deactivate(); + + for (auto& b : _blocks) { + if (b.activated()) { + b.deactivate(); + } + } + } +} + +void +GraphImpl::disable(RunContext& context) +{ + _process = false; + for (auto& o : _outputs) { + o.clear_buffers(context); + } +} + +bool +GraphImpl::prepare_internal_poly(BufferFactory& bufs, uint32_t poly) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + // TODO: Subgraph dynamic polyphony (i.e. changing port polyphony) + + for (auto& b : _blocks) { + b.prepare_poly(bufs, poly); + } + + _poly_pre = poly; + return true; +} + +bool +GraphImpl::apply_internal_poly(RunContext& context, + BufferFactory& bufs, + Raul::Maid& maid, + uint32_t poly) +{ + // TODO: Subgraph dynamic polyphony (i.e. changing port polyphony) + + for (auto& b : _blocks) { + b.apply_poly(context, poly); + } + + for (auto& b : _blocks) { + for (uint32_t j = 0; j < b.num_ports(); ++j) { + PortImpl* const port = b.port_impl(j); + if (port->is_input() && dynamic_cast<InputPort*>(port)->direct_connect()) { + port->setup_buffers(context, bufs, port->poly()); + } + port->connect_buffers(); + } + } + + const bool polyphonic = parent_graph() && (poly == parent_graph()->internal_poly_process()); + for (auto& o : _outputs) { + o.setup_buffers(context, bufs, polyphonic ? poly : 1); + } + + _poly_process = poly; + return true; +} + +void +GraphImpl::pre_process(RunContext& context) +{ + // Mix down input ports and connect buffers + for (uint32_t i = 0; i < num_ports(); ++i) { + PortImpl* const port = _ports->at(i); + if (!port->is_driver_port()) { + port->pre_process(context); + port->pre_run(context); + port->connect_buffers(); + } + } +} + +void +GraphImpl::process(RunContext& context) +{ + if (!_process) { + return; + } + + pre_process(context); + run(context); + post_process(context); +} + +void +GraphImpl::run(RunContext& context) +{ + if (_compiled_graph) { + _compiled_graph->run(context); + } +} + +void +GraphImpl::set_buffer_size(RunContext& context, + BufferFactory& bufs, + LV2_URID type, + uint32_t size) +{ + BlockImpl::set_buffer_size(context, bufs, type, size); + + if (_compiled_graph) { + // FIXME + // for (size_t i = 0; i < _compiled_graph->size(); ++i) { + // const CompiledBlock& block = (*_compiled_graph)[i]; + // block.block()->set_buffer_size(context, bufs, type, size); + // } + } +} + +void +GraphImpl::add_block(BlockImpl& block) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + _blocks.push_front(block); +} + +void +GraphImpl::remove_block(BlockImpl& block) +{ + _blocks.erase(_blocks.iterator_to(block)); +} + +void +GraphImpl::add_arc(SPtr<ArcImpl> a) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + _arcs.emplace(std::make_pair(a->tail(), a->head()), a); +} + +SPtr<ArcImpl> +GraphImpl::remove_arc(const PortImpl* tail, const PortImpl* dst_port) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + auto i = _arcs.find(std::make_pair(tail, dst_port)); + if (i != _arcs.end()) { + SPtr<ArcImpl> arc = dynamic_ptr_cast<ArcImpl>(i->second); + _arcs.erase(i); + return arc; + } else { + return SPtr<ArcImpl>(); + } +} + +bool +GraphImpl::has_arc(const PortImpl* tail, const PortImpl* dst_port) const +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + auto i = _arcs.find(std::make_pair(tail, dst_port)); + return (i != _arcs.end()); +} + +void +GraphImpl::set_compiled_graph(MPtr<CompiledGraph>&& cg) +{ + if (_compiled_graph && _compiled_graph != cg) { + _engine.reset_load(); + } + _compiled_graph = std::move(cg); +} + +uint32_t +GraphImpl::num_ports_non_rt() const +{ + ThreadManager::assert_not_thread(THREAD_PROCESS); + return _inputs.size() + _outputs.size(); +} + +bool +GraphImpl::has_port_with_index(uint32_t index) const +{ + BufferFactory& bufs = *_engine.buffer_factory(); + const auto index_atom = bufs.forge().make(int32_t(index)); + + for (auto p = _inputs.begin(); p != _inputs.end(); ++p) { + if (p->has_property(bufs.uris().lv2_index, index_atom)) { + return true; + } + } + + for (auto p = _outputs.begin(); p != _outputs.end(); ++p) { + if (p->has_property(bufs.uris().lv2_index, index_atom)) { + return true; + } + } + + return false; +} + +void +GraphImpl::remove_port(DuplexPort& port) +{ + if (port.is_input()) { + _inputs.erase(_inputs.iterator_to(port)); + } else { + _outputs.erase(_outputs.iterator_to(port)); + } +} + +void +GraphImpl::clear_ports() +{ + _inputs.clear(); + _outputs.clear(); +} + +MPtr<BlockImpl::Ports> +GraphImpl::build_ports_array(Raul::Maid& maid) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + const size_t n = _inputs.size() + _outputs.size(); + MPtr<Ports> result = maid.make_managed<Ports>(n); + + std::map<size_t, DuplexPort*> ports; + for (PortList::iterator p = _inputs.begin(); p != _inputs.end(); ++p) { + ports.emplace(p->index(), &*p); + } + for (PortList::iterator p = _outputs.begin(); p != _outputs.end(); ++p) { + ports.emplace(p->index(), &*p); + } + + size_t i = 0; + for (const auto& p : ports) { + result->at(i++) = p.second; + } + + assert(i == n); + + return result; +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/GraphImpl.hpp b/src/server/GraphImpl.hpp new file mode 100644 index 00000000..3f11a84a --- /dev/null +++ b/src/server/GraphImpl.hpp @@ -0,0 +1,200 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_GRAPHIMPL_HPP +#define INGEN_ENGINE_GRAPHIMPL_HPP + +#include <cstdlib> + +#include "ingen/ingen.h" + +#include "BlockImpl.hpp" +#include "CompiledGraph.hpp" +#include "DuplexPort.hpp" +#include "PluginImpl.hpp" +#include "PortType.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { + +class Arc; + +namespace Server { + +class ArcImpl; +class CompiledGraph; +class Engine; +class RunContext; + +/** A group of blocks in a graph, possibly polyphonic. + * + * Note that this is also a Block, just one which contains Blocks. + * Therefore infinite subgraphing is possible, of polyphonic + * graphs of polyphonic blocks etc. etc. + * + * \ingroup engine + */ +class GraphImpl : public BlockImpl +{ +public: + GraphImpl(Engine& engine, + const Raul::Symbol& symbol, + uint32_t poly, + GraphImpl* parent, + SampleRate srate, + uint32_t internal_poly); + + virtual ~GraphImpl(); + + virtual GraphType graph_type() const { return GraphType::GRAPH; } + + BlockImpl* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent); + + void activate(BufferFactory& bufs); + void deactivate(); + + void pre_process(RunContext& context); + void process(RunContext& context); + void run(RunContext& context); + + void set_buffer_size(RunContext& context, + BufferFactory& bufs, + LV2_URID type, + uint32_t size); + + /** Prepare for a new (internal) polyphony value. + * + * Pre-process thread, poly is actually applied by apply_internal_poly. + * \return true on success. + */ + bool prepare_internal_poly(BufferFactory& bufs, uint32_t poly); + + /** Apply a new (internal) polyphony value. + * + * Audio thread. + * + * \param context Process context + * \param bufs New set of buffers + * \param poly Must be < the most recent value passed to prepare_internal_poly. + * \param maid Any objects no longer needed will be pushed to this + */ + bool apply_internal_poly(RunContext& context, + BufferFactory& bufs, + Raul::Maid& maid, + uint32_t poly); + + // Graph specific stuff not inherited from Block + + typedef boost::intrusive::slist< + BlockImpl, boost::intrusive::constant_time_size<true> > Blocks; + + /** Add a block to this graph. + * Pre-process thread only. + */ + void add_block(BlockImpl& block); + + /** Remove a block from this graph. + * Pre-process thread only. + */ + void remove_block(BlockImpl& block); + + Blocks& blocks() { return _blocks; } + const Blocks& blocks() const { return _blocks; } + + uint32_t num_ports_non_rt() const; + bool has_port_with_index(uint32_t index) const; + + typedef boost::intrusive::slist< + DuplexPort, boost::intrusive::constant_time_size<true> > PortList; + + void add_input(DuplexPort& port) { + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + assert(port.is_input()); + _inputs.push_front(port); + } + + void add_output(DuplexPort& port) { + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + assert(port.is_output()); + _outputs.push_front(port); + } + + /** Remove port from ports list used in pre-processing thread. + * + * Port is not removed from ports array for process thread (which could be + * simultaneously running). + * + * Pre-processing thread or situations that won't cause races with it only. + */ + void remove_port(DuplexPort& port); + + /** Remove all ports from ports list used in pre-processing thread. + * + * Ports are not removed from ports array for process thread (which could be + * simultaneously running). Returned is a (inputs, outputs) pair. + * + * Pre-processing thread or situations that won't cause races with it only. + */ + void clear_ports(); + + /** Add an arc to this graph. + * Pre-processing thread only. + */ + void add_arc(SPtr<ArcImpl> a); + + /** Remove an arc from this graph. + * Pre-processing thread only. + */ + SPtr<ArcImpl> remove_arc(const PortImpl* tail, const PortImpl* dst_port); + + bool has_arc(const PortImpl* tail, const PortImpl* dst_port) const; + + /** Set a new compiled graph to run, and return the old one. */ + void set_compiled_graph(MPtr<CompiledGraph>&& cg); + + const MPtr<Ports>& external_ports() { return _ports; } + + void set_external_ports(MPtr<Ports>&& pa) { _ports = std::move(pa); } + + MPtr<Ports> build_ports_array(Raul::Maid& maid); + + /** Whether to run this graph's DSP bits in the audio thread */ + bool enabled() const { return _process; } + void enable() { _process = true; } + void disable(RunContext& context); + + uint32_t internal_poly() const { return _poly_pre; } + uint32_t internal_poly_process() const { return _poly_process; } + + Engine& engine() { return _engine; } + +private: + Engine& _engine; + uint32_t _poly_pre; ///< Pre-process thread only + uint32_t _poly_process; ///< Process thread only + MPtr<CompiledGraph> _compiled_graph; ///< Process thread only + PortList _inputs; ///< Pre-process thread only + PortList _outputs; ///< Pre-process thread only + Blocks _blocks; ///< Pre-process thread only + bool _process; ///< True iff graph is enabled +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_GRAPHIMPL_HPP diff --git a/src/server/GraphPlugin.hpp b/src/server/GraphPlugin.hpp new file mode 100644 index 00000000..308ed91a --- /dev/null +++ b/src/server/GraphPlugin.hpp @@ -0,0 +1,63 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_GRAPHPLUGIN_HPP +#define INGEN_ENGINE_GRAPHPLUGIN_HPP + +#include <string> +#include "PluginImpl.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; + +/** Implementation of a Graph plugin. + * + * Graphs don't actually work like this yet... + */ +class GraphPlugin : public PluginImpl +{ +public: + GraphPlugin(URIs& uris, + const URI& uri, + const Raul::Symbol& symbol, + const std::string& name) + : PluginImpl(uris, uris.ingen_Graph.urid, uri) + {} + + BlockImpl* instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state) + { + return nullptr; + } + + const Raul::Symbol symbol() const { return Raul::Symbol("graph"); } + const std::string name() const { return "Ingen Graph"; } + +private: + const std::string _symbol; + const std::string _name; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_GRAPHPLUGIN_HPP diff --git a/src/server/InputPort.cpp b/src/server/InputPort.cpp new file mode 100644 index 00000000..2f22491f --- /dev/null +++ b/src/server/InputPort.cpp @@ -0,0 +1,261 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <cassert> + +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" + +#include "ArcImpl.hpp" +#include "BlockImpl.hpp" +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "RunContext.hpp" +#include "mix.hpp" + +namespace Ingen { +namespace Server { + +InputPort::InputPort(BufferFactory& bufs, + BlockImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size) + : PortImpl(bufs, parent, symbol, index, poly, type, buffer_type, value, buffer_size, false) + , _num_arcs(0) +{ + const Ingen::URIs& uris = bufs.uris(); + + if (parent->graph_type() != Node::GraphType::GRAPH) { + add_property(uris.rdf_type, uris.lv2_InputPort.urid); + } +} + +bool +InputPort::apply_poly(RunContext& context, uint32_t poly) +{ + bool ret = PortImpl::apply_poly(context, poly); + if (!ret) { + poly = 1; + } + + assert(_voices->size() >= poly); + + return true; +} + +bool +InputPort::get_buffers(BufferFactory& bufs, + PortImpl::GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const +{ + if (is_a(PortType::ATOM) && !_value.is_valid()) { + poly = 1; + } + + if (is_a(PortType::AUDIO) && num_in_arcs == 0) { + // Audio input with no arcs, use shared zero buffer + for (uint32_t v = 0; v < poly; ++v) { + voices->at(v).buffer = bufs.silent_buffer(); + } + return false; + } + + // Otherwise, allocate local buffers + for (uint32_t v = 0; v < poly; ++v) { + voices->at(v).buffer.reset(); + voices->at(v).buffer = (bufs.*get)( + buffer_type(), _value.type(), _buffer_size); + voices->at(v).buffer->clear(); + if (_value.is_valid()) { + voices->at(v).buffer->set_value(_value); + } + } + return true; +} + +bool +InputPort::pre_get_buffers(BufferFactory& bufs, + MPtr<Voices>& voices, + uint32_t poly) const +{ + return get_buffers(bufs, &BufferFactory::get_buffer, voices, poly, _num_arcs); +} + +bool +InputPort::setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly) +{ + if (is_a(PortType::ATOM) && !_value.is_valid()) { + poly = 1; + } + + if (_arcs.size() == 1 && !is_a(PortType::ATOM) && !_arcs.front().must_mix()) { + // Single non-mixing connection, use buffers directly + for (uint32_t v = 0; v < poly; ++v) { + _voices->at(v).buffer = _arcs.front().buffer(v); + } + return false; + } + + return get_buffers(bufs, &BufferFactory::claim_buffer, _voices, poly, _arcs.size()); +} + +void +InputPort::add_arc(RunContext& context, ArcImpl& c) +{ + _arcs.push_front(c); +} + +void +InputPort::remove_arc(ArcImpl& arc) +{ + _arcs.erase(_arcs.iterator_to(arc)); +} + +uint32_t +InputPort::max_tail_poly(RunContext& context) const +{ + return parent_block()->parent_graph()->internal_poly_process(); +} + +void +InputPort::pre_process(RunContext& context) +{ + if (_arcs.empty()) { + // No incoming arcs, just handle user-set value + for (uint32_t v = 0; v < _poly; ++v) { + // Update set state + update_set_state(context, v); + + // Prepare for write in case a set event executes this cycle + if (!_parent->is_main()) { + buffer(v)->prepare_write(context); + } + } + } else if (direct_connect()) { + // Directly connected, use source's buffer directly + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer = _arcs.front().buffer(v); + } + } else { + // Mix down to local buffers in pre_run() + for (uint32_t v = 0; v < _poly; ++v) { + buffer(v)->prepare_write(context); + } + } +} + +void +InputPort::pre_run(RunContext& context) +{ + if ((_user_buffer || !_arcs.empty()) && !direct_connect()) { + const uint32_t src_poly = max_tail_poly(context); + const uint32_t max_n_srcs = _arcs.size() * src_poly + 1; + + for (uint32_t v = 0; v < _poly; ++v) { + if (!buffer(v)->get<void>()) { + continue; + } + + // Get all sources for this voice + const Buffer* srcs[max_n_srcs]; + uint32_t n_srcs = 0; + + if (_user_buffer) { + // Add buffer with user/UI input for this cycle + srcs[n_srcs++] = _user_buffer.get(); + } + + for (const auto& arc : _arcs) { + if (_poly == 1) { + // P -> 1 or 1 -> 1: all tail voices => each head voice + for (uint32_t w = 0; w < arc.tail()->poly(); ++w) { + assert(n_srcs < max_n_srcs); + srcs[n_srcs++] = arc.buffer(w, context.offset()).get(); + assert(srcs[n_srcs - 1]); + } + } else { + // P -> P or 1 -> P: tail voice => corresponding head voice + assert(n_srcs < max_n_srcs); + srcs[n_srcs++] = arc.buffer(v, context.offset()).get(); + assert(srcs[n_srcs - 1]); + } + } + + // Then mix them into our buffer for this voice + mix(context, buffer(v).get(), srcs, n_srcs); + update_values(context.offset(), v); + } + } else if (is_a(PortType::CONTROL)) { + for (uint32_t v = 0; v < _poly; ++v) { + update_values(context.offset(), v); + } + } +} + +SampleCount +InputPort::next_value_offset(SampleCount offset, SampleCount end) const +{ + SampleCount earliest = end; + + if (_user_buffer) { + earliest = _user_buffer->next_value_offset(offset, end); + } + + for (const auto& arc : _arcs) { + const SampleCount o = arc.tail()->next_value_offset(offset, end); + if (o < earliest) { + earliest = o; + } + } + + return earliest; +} + +void +InputPort::post_process(RunContext& context) +{ + if (!_arcs.empty() || _force_monitor_update) { + monitor(context, _force_monitor_update); + _force_monitor_update = false; + } + + /* Finished processing any user/UI messages for this cycle, drop reference + to user buffer. */ + _user_buffer.reset(); +} + +bool +InputPort::direct_connect() const +{ + return _arcs.size() == 1 + && !_parent->is_main() + && !_arcs.front().must_mix() + && buffer(0)->type() != _bufs.uris().atom_Sequence; +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/InputPort.hpp b/src/server/InputPort.hpp new file mode 100644 index 00000000..708f7ea2 --- /dev/null +++ b/src/server/InputPort.hpp @@ -0,0 +1,128 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_INPUTPORT_HPP +#define INGEN_ENGINE_INPUTPORT_HPP + +#include <cassert> +#include <cstdlib> + +#include <boost/intrusive/slist.hpp> + +#include "ingen/types.hpp" + +#include "ArcImpl.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +class ArcImpl; +class BlockImpl; +class RunContext; + +/** An input port on a Block or Graph. + * + * All ports have a Buffer, but the actual contents (data) of that buffer may be + * set directly to the incoming arc's buffer if there's only one inbound + * arc, to eliminate the need to copy/mix. + * + * If a port has multiple arcs, they will be mixed down into the local + * buffer and it will be used. + * + * \ingroup engine + */ +class InputPort : public PortImpl +{ +public: + InputPort(BufferFactory& bufs, + BlockImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size = 0); + + typedef boost::intrusive::slist<ArcImpl, + boost::intrusive::constant_time_size<true> + > Arcs; + + /** Return the maximum polyphony of an output connected to this input. */ + virtual uint32_t max_tail_poly(RunContext& context) const; + + bool apply_poly(RunContext& context, uint32_t poly); + + /** Add an arc. Realtime safe. + * + * The buffer of this port will be set directly to the arc's buffer + * if there is only one arc, since no copying/mixing needs to take place. + * + * setup_buffers() must be called later for the change to take effect. + */ + void add_arc(RunContext& context, ArcImpl& c); + + /** Remove an arc. Realtime safe. + * + * setup_buffers() must be called later for the change to take effect. + */ + void remove_arc(ArcImpl& arc); + + /** Like `get_buffers`, but for the pre-process thread. + * + * This uses the "current" number of arcs fromthe perspective of the + * pre-process thread to allocate buffers for application of a + * connection/disconnection/etc in the next process cycle. + */ + bool pre_get_buffers(BufferFactory& bufs, + MPtr<Voices>& voices, + uint32_t poly) const; + + bool setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly); + + /** Set up buffer pointers. */ + void pre_process(RunContext& context); + + /** Prepare buffer for access, mixing if necessary. */ + void pre_run(RunContext& context); + + /** Prepare buffer for next process cycle. */ + void post_process(RunContext& context); + + SampleCount next_value_offset(SampleCount offset, SampleCount end) const; + + size_t num_arcs() const { return _num_arcs; } + void increment_num_arcs() { ++_num_arcs; } + void decrement_num_arcs() { --_num_arcs; } + + bool direct_connect() const; + +protected: + bool get_buffers(BufferFactory& bufs, + PortImpl::GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const; + + size_t _num_arcs; ///< Pre-process thread + Arcs _arcs; ///< Audio thread +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_INPUTPORT_HPP diff --git a/src/server/InternalBlock.cpp b/src/server/InternalBlock.cpp new file mode 100644 index 00000000..3d8f7390 --- /dev/null +++ b/src/server/InternalBlock.cpp @@ -0,0 +1,73 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "InternalBlock.hpp" +#include "InternalPlugin.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +InternalBlock::InternalBlock(PluginImpl* plugin, + const Raul::Symbol& symbol, + bool poly, + GraphImpl* parent, + SampleRate rate) + : BlockImpl(plugin, symbol, poly, parent, rate) +{} + +BlockImpl* +InternalBlock::duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) +{ + BufferFactory& bufs = *engine.buffer_factory(); + + BlockImpl* copy = reinterpret_cast<InternalPlugin*>(_plugin)->instantiate( + bufs, symbol, _polyphonic, parent_graph(), engine, nullptr); + + for (size_t i = 0; i < num_ports(); ++i) { + const Atom& value = port_impl(i)->value(); + copy->port_impl(i)->set_property(bufs.uris().ingen_value, value); + copy->port_impl(i)->set_value(value); + } + + return copy; +} + +void +InternalBlock::pre_process(RunContext& context) +{ + for (uint32_t i = 0; i < num_ports(); ++i) { + PortImpl* const port = _ports->at(i); + if (port->is_input()) { + port->pre_process(context); + } else if (port->buffer_type() == _plugin->uris().atom_Sequence) { + /* Output sequences are initialized in LV2 format, an atom:Chunk + with size set to the capacity of the buffer. Internal nodes + don't care, so clear to an empty sequences so appending events + results in a valid output. */ + for (uint32_t v = 0; v < port->poly(); ++v) { + port->buffer(v)->clear(); + } + } + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/InternalBlock.hpp b/src/server/InternalBlock.hpp new file mode 100644 index 00000000..a57bd89f --- /dev/null +++ b/src/server/InternalBlock.hpp @@ -0,0 +1,48 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_INTERNALBLOCK_HPP +#define INGEN_ENGINE_INTERNALBLOCK_HPP + +#include "BlockImpl.hpp" + +namespace Ingen { +namespace Server { + +/** An internal Block implemented inside Ingen. + * + * \ingroup engine + */ +class InternalBlock : public BlockImpl +{ +public: + InternalBlock(PluginImpl* plugin, + const Raul::Symbol& symbol, + bool poly, + GraphImpl* parent, + SampleRate rate); + + BlockImpl* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent); + + virtual void pre_process(RunContext& context); +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BLOCKIMPL_HPP diff --git a/src/server/InternalPlugin.cpp b/src/server/InternalPlugin.cpp new file mode 100644 index 00000000..6529b9c0 --- /dev/null +++ b/src/server/InternalPlugin.cpp @@ -0,0 +1,67 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" +#include "internals/Controller.hpp" +#include "internals/BlockDelay.hpp" +#include "internals/Note.hpp" +#include "internals/Time.hpp" +#include "internals/Trigger.hpp" + +#include "Engine.hpp" +#include "InternalPlugin.hpp" + +namespace Ingen { +namespace Server { + +using namespace Internals; + +InternalPlugin::InternalPlugin(URIs& uris, + const URI& uri, + const Raul::Symbol& symbol) + : PluginImpl(uris, uris.ingen_Internal.urid, uri) + , _symbol(symbol) +{ + set_property(uris.rdf_type, uris.ingen_Internal); +} + +BlockImpl* +InternalPlugin::instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state) +{ + const SampleCount srate = engine.sample_rate(); + + if (uri() == NS_INTERNALS "BlockDelay") { + return new BlockDelayNode(this, bufs, symbol, polyphonic, parent, srate); + } else if (uri() == NS_INTERNALS "Controller") { + return new ControllerNode(this, bufs, symbol, polyphonic, parent, srate); + } else if (uri() == NS_INTERNALS "Note") { + return new NoteNode(this, bufs, symbol, polyphonic, parent, srate); + } else if (uri() == NS_INTERNALS "Time") { + return new TimeNode(this, bufs, symbol, polyphonic, parent, srate); + } else if (uri() == NS_INTERNALS "Trigger") { + return new TriggerNode(this, bufs, symbol, polyphonic, parent, srate); + } else { + return nullptr; + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/InternalPlugin.hpp b/src/server/InternalPlugin.hpp new file mode 100644 index 00000000..79309beb --- /dev/null +++ b/src/server/InternalPlugin.hpp @@ -0,0 +1,57 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_INTERNALPLUGIN_HPP +#define INGEN_ENGINE_INTERNALPLUGIN_HPP + +#include "raul/Symbol.hpp" + +#include "PluginImpl.hpp" + +#define NS_INTERNALS "http://drobilla.net/ns/ingen-internals#" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class BufferFactory; + +/** Implementation of an Internal plugin. + */ +class InternalPlugin : public PluginImpl +{ +public: + InternalPlugin(URIs& uris, + const URI& uri, + const Raul::Symbol& symbol); + + BlockImpl* instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state); + + const Raul::Symbol symbol() const { return _symbol; } + +private: + const Raul::Symbol _symbol; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_INTERNALPLUGIN_HPP diff --git a/src/server/JackDriver.cpp b/src/server/JackDriver.cpp new file mode 100644 index 00000000..973e3eb7 --- /dev/null +++ b/src/server/JackDriver.cpp @@ -0,0 +1,584 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen_config.h" + +#include <cstdlib> +#include <string> + +#include <jack/midiport.h> +#ifdef INGEN_JACK_SESSION +#include <jack/session.h> +#include <boost/format.hpp> +#include "ingen/Serialiser.hpp" +#endif +#ifdef HAVE_JACK_METADATA +#include <jack/metadata.h> +#include "jackey.h" +#endif + +#include "ingen/Configuration.hpp" +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "Buffer.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "JackDriver.hpp" +#include "PortImpl.hpp" +#include "ThreadManager.hpp" +#include "util.hpp" + +typedef jack_default_audio_sample_t jack_sample_t; + +namespace Ingen { +namespace Server { + +JackDriver::JackDriver(Engine& engine) + : _engine(engine) + , _sem(0) + , _flag(false) + , _client(nullptr) + , _block_length(0) + , _seq_size(0) + , _sample_rate(0) + , _is_activated(false) + , _old_bpm(120.0f) + , _old_frame(0) + , _old_rolling(false) +{ + _midi_event_type = _engine.world()->uris().midi_MidiEvent; + lv2_atom_forge_init( + &_forge, &engine.world()->uri_map().urid_map_feature()->urid_map); +} + +JackDriver::~JackDriver() +{ + deactivate(); + _ports.clear_and_dispose([](EnginePort* p) { delete p; }); +} + +bool +JackDriver::attach(const std::string& server_name, + const std::string& client_name, + void* jack_client) +{ + assert(!_client); + if (!jack_client) { +#ifdef INGEN_JACK_SESSION + const std::string uuid = _engine.world()->jack_uuid(); + if (!uuid.empty()) { + _client = jack_client_open(client_name.c_str(), + JackSessionID, nullptr, + uuid.c_str()); + _engine.log().info(fmt("Connected to Jack as `%1%' (UUID `%2%')\n") + % client_name.c_str() % uuid); + } +#endif + + // Try supplied server name + if (!_client && !server_name.empty()) { + if ((_client = jack_client_open(client_name.c_str(), + JackServerName, nullptr, + server_name.c_str()))) { + _engine.log().info(fmt("Connected to Jack server `%1%'\n") + % server_name); + } + } + + // Either server name not specified, or supplied server name does not exist + // Connect to default server + if (!_client) { + if ((_client = jack_client_open(client_name.c_str(), JackNullOption, nullptr))) { + _engine.log().info("Connected to default Jack server\n"); + } + } + + // Still failed + if (!_client) { + _engine.log().error("Unable to connect to Jack\n"); + return false; + } + } else { + _client = (jack_client_t*)jack_client; + } + + _sample_rate = jack_get_sample_rate(_client); + _block_length = jack_get_buffer_size(_client); + _seq_size = jack_port_type_get_buffer_size(_client, JACK_DEFAULT_MIDI_TYPE); + + _fallback_buffer = AudioBufPtr( + static_cast<float*>( + Buffer::aligned_alloc(sizeof(float) * _block_length))); + + jack_on_shutdown(_client, shutdown_cb, this); + + jack_set_thread_init_callback(_client, thread_init_cb, this); + jack_set_buffer_size_callback(_client, block_length_cb, this); +#ifdef INGEN_JACK_SESSION + jack_set_session_callback(_client, session_cb, this); +#endif + + for (auto& p : _ports) { + register_port(p); + } + + return true; +} + +bool +JackDriver::activate() +{ + World* world = _engine.world(); + + if (_is_activated) { + _engine.log().warn("Jack driver already activated\n"); + return false; + } + + if (!_client) { + attach(world->conf().option("jack-server").ptr<char>(), + world->conf().option("jack-name").ptr<char>(), nullptr); + } + + if (!_client) { + return false; + } + + jack_set_process_callback(_client, process_cb, this); + + _is_activated = true; + + if (jack_activate(_client)) { + _engine.log().error("Could not activate Jack client, aborting\n"); + return false; + } else { + _engine.log().info(fmt("Activated Jack client `%1%'\n") % + world->conf().option("jack-name").ptr<char>()); + } + return true; +} + +void +JackDriver::deactivate() +{ + if (_is_activated) { + _flag = true; + _is_activated = false; + _sem.timed_wait(std::chrono::seconds(1)); + + for (auto& p : _ports) { + unregister_port(p); + } + + if (_client) { + jack_deactivate(_client); + jack_client_close(_client); + _client = nullptr; + } + + _engine.log().info("Deactivated Jack client\n"); + } +} + +EnginePort* +JackDriver::get_port(const Raul::Path& path) +{ + for (auto& p : _ports) { + if (p.graph_port()->path() == path) { + return &p; + } + } + + return nullptr; +} + +void +JackDriver::add_port(RunContext& context, EnginePort* port) +{ + _ports.push_back(*port); + + DuplexPort* graph_port = port->graph_port(); + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + const SampleCount nframes = context.nframes(); + jack_port_t* jport = (jack_port_t*)port->handle(); + void* jbuf = jack_port_get_buffer(jport, nframes); + + /* Jack fails to return a buffer if this is too soon after registering + the port, so use a silent fallback buffer if necessary. */ + graph_port->set_driver_buffer( + jbuf ? jbuf : _fallback_buffer.get(), + nframes * sizeof(float)); + } +} + +void +JackDriver::remove_port(RunContext& context, EnginePort* port) +{ + _ports.erase(_ports.iterator_to(*port)); +} + +void +JackDriver::register_port(EnginePort& port) +{ + jack_port_t* jack_port = jack_port_register( + _client, + port.graph_port()->path().substr(1).c_str(), + ((port.graph_port()->is_a(PortType::AUDIO) || + port.graph_port()->is_a(PortType::CV)) + ? JACK_DEFAULT_AUDIO_TYPE : JACK_DEFAULT_MIDI_TYPE), + (port.graph_port()->is_input() + ? JackPortIsInput : JackPortIsOutput), + 0); + + if (!jack_port) { + throw JackDriver::PortRegistrationFailedException(); + } + + port.set_handle(jack_port); + + for (const auto& p : port.graph_port()->properties()) { + port_property_internal(jack_port, p.first, p.second); + } +} + +void +JackDriver::unregister_port(EnginePort& port) +{ + if (jack_port_unregister(_client, (jack_port_t*)port.handle())) { + _engine.log().error("Failed to unregister Jack port\n"); + } + + port.set_handle(nullptr); +} + +void +JackDriver::rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) +{ + EnginePort* eport = get_port(old_path); + if (eport) { +#ifdef HAVE_JACK_PORT_RENAME + jack_port_rename( + _client, (jack_port_t*)eport->handle(), new_path.substr(1).c_str()); +#else + jack_port_set_name((jack_port_t*)eport->handle(), + new_path.substr(1).c_str()); +#endif + } +} + +void +JackDriver::port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) +{ +#ifdef HAVE_JACK_METADATA + EnginePort* eport = get_port(path); + if (eport) { + const jack_port_t* const jport = (const jack_port_t*)eport->handle(); + port_property_internal(jport, uri, value); + } +#endif +} + +void +JackDriver::port_property_internal(const jack_port_t* jport, + const URI& uri, + const Atom& value) +{ +#ifdef HAVE_JACK_METADATA + if (uri == _engine.world()->uris().lv2_name) { + jack_set_property(_client, jack_port_uuid(jport), + JACK_METADATA_PRETTY_NAME, value.ptr<char>(), "text/plain"); + } else if (uri == _engine.world()->uris().lv2_index) { + jack_set_property(_client, jack_port_uuid(jport), + JACKEY_ORDER, std::to_string(value.get<int32_t>()).c_str(), + "http://www.w3.org/2001/XMLSchema#integer"); + } else if (uri == _engine.world()->uris().rdf_type) { + if (value == _engine.world()->uris().lv2_CVPort) { + jack_set_property(_client, jack_port_uuid(jport), + JACKEY_SIGNAL_TYPE, "CV", "text/plain"); + } + } +#endif +} + +EnginePort* +JackDriver::create_port(DuplexPort* graph_port) +{ + EnginePort* eport = nullptr; + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + // Audio buffer port, use Jack buffer directly + eport = new EnginePort(graph_port); + graph_port->set_is_driver_port(*_engine.buffer_factory()); + } else if (graph_port->is_a(PortType::ATOM) && + graph_port->buffer_type() == _engine.world()->uris().atom_Sequence) { + // Sequence port, make Jack port but use internal LV2 format buffer + eport = new EnginePort(graph_port); + } + + if (eport) { + register_port(*eport); + } + + return eport; +} + +void +JackDriver::pre_process_port(RunContext& context, EnginePort* port) +{ + const URIs& uris = context.engine().world()->uris(); + const SampleCount nframes = context.nframes(); + jack_port_t* jack_port = (jack_port_t*)port->handle(); + DuplexPort* graph_port = port->graph_port(); + Buffer* graph_buf = graph_port->buffer(0).get(); + void* jack_buf = jack_port_get_buffer(jack_port, nframes); + + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + graph_port->set_driver_buffer(jack_buf, nframes * sizeof(float)); + if (graph_port->is_input()) { + graph_port->monitor(context); + } else { + graph_port->buffer(0)->clear(); // TODO: Avoid when possible + } + } else if (graph_port->buffer_type() == uris.atom_Sequence) { + graph_buf->prepare_write(context); + if (graph_port->is_input()) { + // Copy events from Jack port buffer into graph port buffer + const jack_nframes_t event_count = jack_midi_get_event_count(jack_buf); + for (jack_nframes_t i = 0; i < event_count; ++i) { + jack_midi_event_t ev; + jack_midi_event_get(&ev, jack_buf, i); + if (!graph_buf->append_event( + ev.time, ev.size, _midi_event_type, ev.buffer)) { + _engine.log().rt_error("Failed to write to MIDI buffer, events lost!\n"); + } + } + } + graph_port->monitor(context); + } +} + +void +JackDriver::post_process_port(RunContext& context, EnginePort* port) +{ + const URIs& uris = context.engine().world()->uris(); + const SampleCount nframes = context.nframes(); + jack_port_t* jack_port = (jack_port_t*)port->handle(); + DuplexPort* graph_port = port->graph_port(); + void* jack_buf = port->buffer(); + + if (port->graph_port()->is_output()) { + if (!jack_buf) { + // First cycle for a new output, so pre_process wasn't called + jack_buf = jack_port_get_buffer(jack_port, nframes); + port->set_buffer(jack_buf); + } + + if (graph_port->buffer_type() == uris.atom_Sequence) { + // Copy LV2 MIDI events to Jack MIDI buffer + Buffer* const graph_buf = graph_port->buffer(0).get(); + LV2_Atom_Sequence* seq = graph_buf->get<LV2_Atom_Sequence>(); + + jack_midi_clear_buffer(jack_buf); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY(&ev->body); + if (ev->body.type == _midi_event_type) { + jack_midi_event_write( + jack_buf, ev->time.frames, buf, ev->body.size); + } + } + } + } + + // Reset graph port buffer pointer to no longer point to the Jack buffer + if (graph_port->is_driver_port()) { + graph_port->set_driver_buffer(nullptr, 0); + } +} + +void +JackDriver::append_time_events(RunContext& context, + Buffer& buffer) +{ + const URIs& uris = context.engine().world()->uris(); + const jack_position_t* pos = &_position; + const bool rolling = (_transport_state == JackTransportRolling); + + // Do nothing if there is no unexpected time change (other than rolling) + if (rolling == _old_rolling && + pos->frame == _old_frame && + pos->beats_per_minute == _old_bpm) { + return; + } + + // Update old time information to detect change next cycle + _old_frame = pos->frame; + _old_rolling = rolling; + _old_bpm = pos->beats_per_minute; + + // Build an LV2 position object to append to the buffer + LV2_Atom pos_buf[16]; + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_set_buffer(&_forge, (uint8_t*)pos_buf, sizeof(pos_buf)); + lv2_atom_forge_object(&_forge, &frame, 0, uris.time_Position); + lv2_atom_forge_key(&_forge, uris.time_frame); + lv2_atom_forge_long(&_forge, pos->frame); + lv2_atom_forge_key(&_forge, uris.time_speed); + lv2_atom_forge_float(&_forge, rolling ? 1.0 : 0.0); + if (pos->valid & JackPositionBBT) { + lv2_atom_forge_key(&_forge, uris.time_barBeat); + lv2_atom_forge_float( + &_forge, pos->beat - 1 + (pos->tick / pos->ticks_per_beat)); + lv2_atom_forge_key(&_forge, uris.time_bar); + lv2_atom_forge_long(&_forge, pos->bar - 1); + lv2_atom_forge_key(&_forge, uris.time_beatUnit); + lv2_atom_forge_int(&_forge, pos->beat_type); + lv2_atom_forge_key(&_forge, uris.time_beatsPerBar); + lv2_atom_forge_float(&_forge, pos->beats_per_bar); + lv2_atom_forge_key(&_forge, uris.time_beatsPerMinute); + lv2_atom_forge_float(&_forge, pos->beats_per_minute); + } + + // Append position to buffer at offset 0 (start of this cycle) + LV2_Atom* lpos = (LV2_Atom*)pos_buf; + buffer.append_event( + 0, lpos->size, lpos->type, (const uint8_t*)LV2_ATOM_BODY_CONST(lpos)); +} + +/**** Jack Callbacks ****/ + +/** Jack process callback, drives entire audio thread. + * + * \callgraph + */ +REALTIME int +JackDriver::_process_cb(jack_nframes_t nframes) +{ + if (nframes == 0 || ! _is_activated) { + if (_flag) { + _sem.post(); + } + return 0; + } + + /* Note that Jack may not call this function for a cycle, if overloaded, + so a rolling counter here would not always be correct. */ + const jack_nframes_t start_of_current_cycle = jack_last_frame_time(_client); + + _transport_state = jack_transport_query(_client, &_position); + + _engine.locate(start_of_current_cycle, nframes); + + // Read input + for (auto& p : _ports) { + pre_process_port(_engine.run_context(), &p); + } + + // Process + _engine.run(nframes); + + // Write output + for (auto& p : _ports) { + post_process_port(_engine.run_context(), &p); + } + + // Update expected transport frame for next cycle to detect changes + if (_transport_state == JackTransportRolling) { + _old_frame += nframes; + } + + return 0; +} + +void +JackDriver::_thread_init_cb() +{ + ThreadManager::set_flag(THREAD_PROCESS); + ThreadManager::set_flag(THREAD_IS_REAL_TIME); +} + +void +JackDriver::_shutdown_cb() +{ + _engine.log().info("Jack shutdown, exiting\n"); + _is_activated = false; + _client = nullptr; +} + +int +JackDriver::_block_length_cb(jack_nframes_t nframes) +{ + if (_engine.root_graph()) { + _block_length = nframes; + _seq_size = jack_port_type_get_buffer_size(_client, JACK_DEFAULT_MIDI_TYPE); + _engine.root_graph()->set_buffer_size( + _engine.run_context(), *_engine.buffer_factory(), PortType::AUDIO, + _engine.buffer_factory()->audio_buffer_size(nframes)); + _engine.root_graph()->set_buffer_size( + _engine.run_context(), *_engine.buffer_factory(), PortType::ATOM, + _seq_size); + } + return 0; +} + +#ifdef INGEN_JACK_SESSION +void +JackDriver::_session_cb(jack_session_event_t* event) +{ + _engine.log().info(fmt("Jack session save to %1%\n") % event->session_dir); + + const std::string cmd = ( + boost::format("ingen -eg -n %1% -u %2% -l ${SESSION_DIR}") + % jack_get_client_name(_client) + % event->client_uuid).str(); + + SPtr<Serialiser> serialiser = _engine.world()->serialiser(); + if (serialiser) { + std::lock_guard<std::mutex> lock(_engine.world()->rdf_mutex()); + + SPtr<Node> root(_engine.root_graph(), NullDeleter<Node>); + serialiser->write_bundle(root, + URI(std::string("file://") + event->session_dir)); + } + + event->command_line = (char*)malloc(cmd.size() + 1); + memcpy(event->command_line, cmd.c_str(), cmd.size() + 1); + jack_session_reply(_client, event); + + switch (event->type) { + case JackSessionSave: + break; + case JackSessionSaveAndQuit: + _engine.log().warn("Jack session quit\n"); + _engine.quit(); + break; + case JackSessionSaveTemplate: + break; + } + + jack_session_event_free(event); +} +#endif + +} // namespace Server +} // namespace Ingen diff --git a/src/server/JackDriver.hpp b/src/server/JackDriver.hpp new file mode 100644 index 00000000..2a21d96e --- /dev/null +++ b/src/server/JackDriver.hpp @@ -0,0 +1,169 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_JACKAUDIODRIVER_HPP +#define INGEN_ENGINE_JACKAUDIODRIVER_HPP + +#include "ingen_config.h" + +#include <string> +#include <atomic> + +#include <jack/jack.h> +#include <jack/thread.h> +#include <jack/transport.h> +#ifdef INGEN_JACK_SESSION +#include <jack/session.h> +#endif + +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "raul/Semaphore.hpp" + +#include "Driver.hpp" +#include "EnginePort.hpp" + +namespace Raul { class Path; } + +namespace Ingen { +namespace Server { + +class DuplexPort; +class Engine; +class GraphImpl; +class JackDriver; +class PortImpl; + +/** The Jack Driver. + * + * The process callback here drives the entire audio thread by "pulling" + * events from queues, processing them, running the graphs, and passing + * events along to the PostProcessor. + * + * \ingroup engine + */ +class JackDriver : public Driver +{ +public: + explicit JackDriver(Engine& engine); + ~JackDriver(); + + bool attach(const std::string& server_name, + const std::string& client_name, + void* jack_client); + + bool activate(); + void deactivate(); + + bool dynamic_ports() const { return true; } + + EnginePort* create_port(DuplexPort* graph_port); + EnginePort* get_port(const Raul::Path& path); + + void rename_port(const Raul::Path& old_path, const Raul::Path& new_path); + void port_property(const Raul::Path& path, const URI& uri, const Atom& value); + void add_port(RunContext& context, EnginePort* port); + void remove_port(RunContext& context, EnginePort* port); + void register_port(EnginePort& port); + void unregister_port(EnginePort& port); + + /** Transport state for this frame. + * Intended to only be called from the audio thread. */ + inline const jack_position_t* position() { return &_position; } + inline jack_transport_state_t transport_state() { return _transport_state; } + + void append_time_events(RunContext& context, + Buffer& buffer); + + int real_time_priority() { return jack_client_real_time_priority(_client); } + + jack_client_t* jack_client() const { return _client; } + SampleCount block_length() const { return _block_length; } + size_t seq_size() const { return _seq_size; } + SampleCount sample_rate() const { return _sample_rate; } + + inline SampleCount frame_time() const { return _client ? jack_frame_time(_client) : 0; } + + class PortRegistrationFailedException : public std::exception {}; + +private: + friend class JackPort; + + // Static JACK callbacks which call the non-static callbacks (methods) + inline static void thread_init_cb(void* const jack_driver) { + return ((JackDriver*)jack_driver)->_thread_init_cb(); + } + inline static void shutdown_cb(void* const jack_driver) { + return ((JackDriver*)jack_driver)->_shutdown_cb(); + } + inline static int process_cb(jack_nframes_t nframes, void* const jack_driver) { + return ((JackDriver*)jack_driver)->_process_cb(nframes); + } + inline static int block_length_cb(jack_nframes_t nframes, void* const jack_driver) { + return ((JackDriver*)jack_driver)->_block_length_cb(nframes); + } +#ifdef INGEN_JACK_SESSION + inline static void session_cb(jack_session_event_t* event, void* jack_driver) { + ((JackDriver*)jack_driver)->_session_cb(event); + } +#endif + + void pre_process_port(RunContext& context, EnginePort* port); + void post_process_port(RunContext& context, EnginePort* port); + + void port_property_internal(const jack_port_t* jport, + const URI& uri, + const Atom& value); + + // Non static callbacks (methods) + void _thread_init_cb(); + void _shutdown_cb(); + int _process_cb(jack_nframes_t nframes); + int _block_length_cb(jack_nframes_t nframes); +#ifdef INGEN_JACK_SESSION + void _session_cb(jack_session_event_t* event); +#endif + +protected: + typedef boost::intrusive::slist<EnginePort, + boost::intrusive::cache_last<true> + > Ports; + + using AudioBufPtr = UPtr<float, FreeDeleter<float>>; + + Engine& _engine; + Ports _ports; + AudioBufPtr _fallback_buffer; + LV2_Atom_Forge _forge; + Raul::Semaphore _sem; + std::atomic<bool> _flag; + jack_client_t* _client; + jack_nframes_t _block_length; + size_t _seq_size; + jack_nframes_t _sample_rate; + uint32_t _midi_event_type; + bool _is_activated; + jack_position_t _position; + jack_transport_state_t _transport_state; + float _old_bpm; + jack_nframes_t _old_frame; + bool _old_rolling; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_JACKAUDIODRIVER_HPP diff --git a/src/server/LV2Block.cpp b/src/server/LV2Block.cpp new file mode 100644 index 00000000..f4792f39 --- /dev/null +++ b/src/server/LV2Block.cpp @@ -0,0 +1,742 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cmath> +#include <cstdint> + +#include "lv2/lv2plug.in/ns/ext/morph/morph.h" +#include "lv2/lv2plug.in/ns/ext/presets/presets.h" +#include "lv2/lv2plug.in/ns/ext/options/options.h" +#include "lv2/lv2plug.in/ns/ext/resize-port/resize-port.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" + +#include "raul/Maid.hpp" +#include "raul/Array.hpp" + +#include "ingen/FilePath.hpp" +#include "ingen/Log.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "LV2Block.hpp" +#include "LV2Plugin.hpp" +#include "OutputPort.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "Worker.hpp" + +namespace Ingen { +namespace Server { + +/** Partially construct a LV2Block. + * + * Object is not usable until instantiate() is called with success. + * (It _will_ crash!) + */ +LV2Block::LV2Block(LV2Plugin* plugin, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : BlockImpl(plugin, symbol, polyphonic, parent, srate) + , _lv2_plugin(plugin) + , _worker_iface(nullptr) +{ + assert(_lv2_plugin); +} + +LV2Block::~LV2Block() +{ + // Explicitly drop instances first to prevent reference cycles + drop_instances(_instances); + drop_instances(_prepared_instances); +} + +SPtr<LV2Block::Instance> +LV2Block::make_instance(URIs& uris, + SampleRate rate, + uint32_t voice, + bool preparing) +{ + const Engine& engine = parent_graph()->engine(); + const LilvPlugin* lplug = _lv2_plugin->lilv_plugin(); + LilvInstance* inst = lilv_plugin_instantiate( + lplug, rate, _features->array()); + + if (!inst) { + engine.log().error(fmt("Failed to instantiate <%1%>\n") + % _lv2_plugin->uri().c_str()); + return SPtr<Instance>(); + } + + const LV2_Options_Interface* options_iface = nullptr; + if (lilv_plugin_has_extension_data(lplug, uris.opt_interface)) { + options_iface = (const LV2_Options_Interface*) + lilv_instance_get_extension_data(inst, LV2_OPTIONS__interface); + } + + for (uint32_t p = 0; p < num_ports(); ++p) { + PortImpl* const port = _ports->at(p); + Buffer* const buffer = (preparing) + ? port->prepared_buffer(voice).get() + : port->buffer(voice).get(); + if (port->is_morph() && port->is_a(PortType::CV)) { + if (options_iface) { + const LV2_URID port_type = uris.lv2_CVPort; + const LV2_Options_Option options[] = { + { LV2_OPTIONS_PORT, p, uris.morph_currentType, + sizeof(LV2_URID), uris.atom_URID, &port_type }, + { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr } + }; + options_iface->set(inst->lv2_handle, options); + } + } + + if (buffer) { + if (port->is_a(PortType::CONTROL)) { + buffer->set_value(port->value()); + } else if (port->is_a(PortType::CV)) { + buffer->set_block(port->value().get<float>(), 0, engine.block_length()); + } else { + buffer->clear(); + } + } + } + + if (options_iface) { + for (uint32_t p = 0; p < num_ports(); ++p) { + PortImpl* const port = _ports->at(p); + if (port->is_auto_morph()) { + LV2_Options_Option options[] = { + { LV2_OPTIONS_PORT, p, uris.morph_currentType, 0, 0, nullptr }, + { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr } + }; + + options_iface->get(inst->lv2_handle, options); + if (options[0].value) { + LV2_URID type = *(const LV2_URID*)options[0].value; + if (type == _uris.lv2_ControlPort) { + port->set_type(PortType::CONTROL, 0); + } else if (type == _uris.lv2_CVPort) { + port->set_type(PortType::CV, 0); + } else { + parent_graph()->engine().log().error( + fmt("%1% auto-morphed to unknown type %2%\n") + % port->path().c_str() % type); + return SPtr<Instance>(); + } + } else { + parent_graph()->engine().log().error( + fmt("Failed to get auto-morphed type of %1%\n") + % port->path().c_str()); + } + } + } + } + + return std::make_shared<Instance>(inst); +} + +bool +LV2Block::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + if (!_polyphonic) { + poly = 1; + } + + BlockImpl::prepare_poly(bufs, poly); + + if (_polyphony == poly) { + return true; + } + + const SampleRate rate = bufs.engine().sample_rate(); + assert(!_prepared_instances); + _prepared_instances = bufs.maid().make_managed<Instances>( + poly, *_instances, SPtr<Instance>()); + for (uint32_t i = _polyphony; i < _prepared_instances->size(); ++i) { + SPtr<Instance> inst = make_instance(bufs.uris(), rate, i, true); + if (!inst) { + _prepared_instances.reset(); + return false; + } + + _prepared_instances->at(i) = inst; + + if (_activated) { + lilv_instance_activate(inst->instance); + } + } + + return true; +} + +bool +LV2Block::apply_poly(RunContext& context, uint32_t poly) +{ + if (!_polyphonic) { + poly = 1; + } + + if (_prepared_instances) { + _instances = std::move(_prepared_instances); + } + assert(poly <= _instances->size()); + + return BlockImpl::apply_poly(context, poly); +} + +/** Instantiate self from LV2 plugin descriptor. + * + * Implemented as a seperate function (rather than in the constructor) to + * allow graceful error-catching of broken plugins. + * + * Returns whether or not plugin was successfully instantiated. If return + * value is false, this object may not be used. + */ +bool +LV2Block::instantiate(BufferFactory& bufs, const LilvState* state) +{ + const Ingen::URIs& uris = bufs.uris(); + Ingen::World* world = bufs.engine().world(); + const LilvPlugin* plug = _lv2_plugin->lilv_plugin(); + Ingen::Forge& forge = bufs.forge(); + const uint32_t num_ports = lilv_plugin_get_num_ports(plug); + + LilvNode* lv2_connectionOptional = lilv_new_uri( + bufs.engine().world()->lilv_world(), LV2_CORE__connectionOptional); + + _ports = bufs.maid().make_managed<BlockImpl::Ports>(num_ports, nullptr); + + bool ret = true; + + float* min_values = new float[num_ports]; + float* max_values = new float[num_ports]; + float* def_values = new float[num_ports]; + lilv_plugin_get_port_ranges_float(plug, min_values, max_values, def_values); + uint32_t max_sequence_size = 0; + + // Get all the necessary information about ports + for (uint32_t j = 0; j < num_ports; ++j) { + const LilvPort* id = lilv_plugin_get_port_by_index(plug, j); + + /* LV2 port symbols are guaranteed to be unique, valid C identifiers, + and Lilv guarantees that lilv_port_get_symbol() is valid. */ + const Raul::Symbol port_sym( + lilv_node_as_string(lilv_port_get_symbol(plug, id))); + + // Get port type + Atom val; + PortType port_type = PortType::UNKNOWN; + LV2_URID buffer_type = 0; + bool is_morph = false; + bool is_auto_morph = false; + if (lilv_port_is_a(plug, id, uris.lv2_ControlPort)) { + if (lilv_port_is_a(plug, id, uris.morph_MorphPort)) { + is_morph = true; + LilvNodes* types = lilv_port_get_value( + plug, id, uris.morph_supportsType); + LILV_FOREACH(nodes, i, types) { + const LilvNode* type = lilv_nodes_get(types, i); + if (lilv_node_equals(type, uris.lv2_CVPort)) { + port_type = PortType::CV; + buffer_type = uris.atom_Sound; + } + } + lilv_nodes_free(types); + } + if (port_type == PortType::UNKNOWN) { + port_type = PortType::CONTROL; + buffer_type = uris.atom_Sequence; + val = forge.make(def_values[j]); + } + } else if (lilv_port_is_a(plug, id, uris.lv2_CVPort)) { + port_type = PortType::CV; + buffer_type = uris.atom_Sound; + } else if (lilv_port_is_a(plug, id, uris.lv2_AudioPort)) { + port_type = PortType::AUDIO; + buffer_type = uris.atom_Sound; + } else if (lilv_port_is_a(plug, id, uris.atom_AtomPort)) { + port_type = PortType::ATOM; + } + + if (lilv_port_is_a(plug, id, uris.morph_AutoMorphPort)) { + is_auto_morph = true; + } + + // Get buffer type if necessary (atom ports) + if (!buffer_type) { + LilvNodes* types = lilv_port_get_value( + plug, id, uris.atom_bufferType); + LILV_FOREACH(nodes, i, types) { + const LilvNode* type = lilv_nodes_get(types, i); + if (lilv_node_is_uri(type)) { + buffer_type = bufs.engine().world()->uri_map().map_uri( + lilv_node_as_uri(type)); + } + } + lilv_nodes_free(types); + } + + const bool optional = lilv_port_has_property( + plug, id, lv2_connectionOptional); + + uint32_t port_buffer_size = bufs.default_size(buffer_type); + if (port_buffer_size == 0 && !optional) { + parent_graph()->engine().log().error( + fmt("<%1%> port `%2%' has unknown buffer type\n") + % _lv2_plugin->uri().c_str() % port_sym.c_str()); + ret = false; + break; + } + + if (port_type == PortType::ATOM) { + // Get default value, and its length + LilvNodes* defaults = lilv_port_get_value(plug, id, uris.lv2_default); + LILV_FOREACH(nodes, i, defaults) { + const LilvNode* d = lilv_nodes_get(defaults, i); + if (lilv_node_is_string(d)) { + const char* str_val = lilv_node_as_string(d); + const uint32_t str_val_len = strlen(str_val); + val = forge.alloc(str_val); + port_buffer_size = std::max(port_buffer_size, str_val_len); + } else if (lilv_node_is_uri(d)) { + const char* uri_val = lilv_node_as_uri(d); + val = forge.make_urid( + bufs.engine().world()->uri_map().map_uri(uri_val)); + } + } + lilv_nodes_free(defaults); + + if (!val.type() && buffer_type == _uris.atom_URID) { + val = forge.make_urid(0); + } + + // Get minimum size, if set in data + LilvNodes* sizes = lilv_port_get_value(plug, id, uris.rsz_minimumSize); + LILV_FOREACH(nodes, i, sizes) { + const LilvNode* d = lilv_nodes_get(sizes, i); + if (lilv_node_is_int(d)) { + uint32_t size_val = lilv_node_as_int(d); + port_buffer_size = std::max(port_buffer_size, size_val); + } + } + lilv_nodes_free(sizes); + max_sequence_size = std::max(port_buffer_size, max_sequence_size); + bufs.set_seq_size(max_sequence_size); + } + + enum { UNKNOWN, INPUT, OUTPUT } direction = UNKNOWN; + if (lilv_port_is_a(plug, id, uris.lv2_InputPort)) { + direction = INPUT; + } else if (lilv_port_is_a(plug, id, uris.lv2_OutputPort)) { + direction = OUTPUT; + } + + if ((port_type == PortType::UNKNOWN && !optional) || + direction == UNKNOWN) { + parent_graph()->engine().log().error( + fmt("<%1%> port `%2%' has unknown type or direction\n") + % _lv2_plugin->uri().c_str() % port_sym.c_str()); + ret = false; + break; + } + + if (!val.type() && (port_type != PortType::ATOM)) { + // Ensure numeric ports have a value, use 0 by default + val = forge.make(std::isnan(def_values[j]) ? 0.0f : def_values[j]); + } + + PortImpl* port = (direction == INPUT) + ? static_cast<PortImpl*>( + new InputPort(bufs, this, port_sym, j, _polyphony, + port_type, buffer_type, val)) + : static_cast<PortImpl*>( + new OutputPort(bufs, this, port_sym, j, _polyphony, + port_type, buffer_type, val)); + + port->set_morphable(is_morph, is_auto_morph); + if (direction == INPUT && (port_type == PortType::CONTROL + || port_type == PortType::CV)) { + port->set_value(val); + if (!std::isnan(min_values[j])) { + port->set_minimum(forge.make(min_values[j])); + } + if (!std::isnan(max_values[j])) { + port->set_maximum(forge.make(max_values[j])); + } + } + + // Inherit certain properties from plugin port + const LilvNode* preds[] = { uris.lv2_designation, + uris.lv2_portProperty, + uris.atom_supports, + nullptr }; + for (int p = 0; preds[p]; ++p) { + LilvNodes* values = lilv_port_get_value(plug, id, preds[p]); + LILV_FOREACH(nodes, v, values) { + const LilvNode* val = lilv_nodes_get(values, v); + if (lilv_node_is_uri(val)) { + port->add_property(URI(lilv_node_as_uri(preds[p])), + forge.make_urid(URI(lilv_node_as_uri(val)))); + } + } + lilv_nodes_free(values); + } + + port->cache_properties(); + + _ports->at(j) = port; + } + + delete[] min_values; + delete[] max_values; + delete[] def_values; + + lilv_node_free(lv2_connectionOptional); + + if (!ret) { + _ports.reset(); + return ret; + } + + _features = world->lv2_features().lv2_features(world, this); + + // Actually create plugin instances and port buffers. + const SampleRate rate = bufs.engine().sample_rate(); + _instances = bufs.maid().make_managed<Instances>( + _polyphony, SPtr<Instance>()); + for (uint32_t i = 0; i < _polyphony; ++i) { + _instances->at(i) = make_instance(bufs.uris(), rate, i, false); + if (!_instances->at(i)) { + return false; + } + } + + // Load initial state if no state is explicitly given + LilvState* default_state = nullptr; + if (!state) { + state = default_state = load_preset(_lv2_plugin->uri()); + } + + // Apply state + if (state) { + apply_state(nullptr, state); + } + + if (default_state) { + lilv_state_free(default_state); + } + + // FIXME: Polyphony + worker? + if (lilv_plugin_has_feature(plug, uris.work_schedule)) { + _worker_iface = (const LV2_Worker_Interface*) + lilv_instance_get_extension_data(instance(0), + LV2_WORKER__interface); + } + + return ret; +} + +bool +LV2Block::save_state(const FilePath& dir) const +{ + World* world = _lv2_plugin->world(); + LilvWorld* lworld = world->lilv_world(); + + LilvState* state = lilv_state_new_from_instance( + _lv2_plugin->lilv_plugin(), const_cast<LV2Block*>(this)->instance(0), + &world->uri_map().urid_map_feature()->urid_map, + nullptr, dir.c_str(), dir.c_str(), dir.c_str(), nullptr, nullptr, + LV2_STATE_IS_POD|LV2_STATE_IS_PORTABLE, nullptr); + + if (!state) { + return false; + } else if (lilv_state_get_num_properties(state) == 0) { + lilv_state_free(state); + return false; + } + + lilv_state_save(lworld, + &world->uri_map().urid_map_feature()->urid_map, + &world->uri_map().urid_unmap_feature()->urid_unmap, + state, + nullptr, + dir.c_str(), + "state.ttl"); + + lilv_state_free(state); + + return true; +} + +BlockImpl* +LV2Block::duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) +{ + const SampleRate rate = engine.sample_rate(); + + // Get current state + LilvState* state = lilv_state_new_from_instance( + _lv2_plugin->lilv_plugin(), instance(0), + &engine.world()->uri_map().urid_map_feature()->urid_map, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, LV2_STATE_IS_NATIVE, nullptr); + + // Duplicate and instantiate block + LV2Block* dup = new LV2Block(_lv2_plugin, symbol, _polyphonic, parent, rate); + if (!dup->instantiate(*engine.buffer_factory(), state)) { + delete dup; + return nullptr; + } + dup->set_properties(properties()); + + // Set duplicate port values and properties to the same as ours + for (uint32_t p = 0; p < num_ports(); ++p) { + const Atom& val = port_impl(p)->value(); + if (val.is_valid()) { + dup->port_impl(p)->set_value(val); + } + dup->port_impl(p)->set_properties(port_impl(p)->properties()); + } + + return dup; +} + +void +LV2Block::activate(BufferFactory& bufs) +{ + BlockImpl::activate(bufs); + + for (uint32_t i = 0; i < _polyphony; ++i) { + lilv_instance_activate(instance(i)); + } +} + +void +LV2Block::deactivate() +{ + BlockImpl::deactivate(); + + for (uint32_t i = 0; i < _polyphony; ++i) { + lilv_instance_deactivate(instance(i)); + } +} + +LV2_Worker_Status +LV2Block::work_respond(LV2_Worker_Respond_Handle handle, + uint32_t size, + const void* data) +{ + LV2Block* block = (LV2Block*)handle; + LV2Block::Response* r = new LV2Block::Response(size, data); + block->_responses.push_back(*r); + return LV2_WORKER_SUCCESS; +} + +LV2_Worker_Status +LV2Block::work(uint32_t size, const void* data) +{ + if (_worker_iface) { + std::lock_guard<std::mutex> lock(_work_mutex); + + LV2_Handle inst = lilv_instance_get_handle(instance(0)); + LV2_Worker_Status st = _worker_iface->work(inst, work_respond, this, size, data); + if (st) { + parent_graph()->engine().log().error( + fmt("Error calling %1% work method\n") % _path); + } + return st; + } + return LV2_WORKER_ERR_UNKNOWN; +} + +void +LV2Block::run(RunContext& context) +{ + for (uint32_t i = 0; i < _polyphony; ++i) { + lilv_instance_run(instance(i), context.nframes()); + } +} + +void +LV2Block::post_process(RunContext& context) +{ + /* Handle any worker responses. Note that this may write to output ports, + so must be done first to prevent clobbering worker responses and + monitored notification ports. */ + if (_worker_iface) { + LV2_Handle inst = lilv_instance_get_handle(instance(0)); + while (!_responses.empty()) { + Response& r = _responses.front(); + _worker_iface->work_response(inst, r.size, r.data); + _responses.pop_front(); + context.engine().maid()->dispose(&r); + } + + if (_worker_iface->end_run) { + _worker_iface->end_run(inst); + } + } + + /* Run cycle truly finished, finalise output ports. */ + BlockImpl::post_process(context); +} + +LilvState* +LV2Block::load_preset(const URI& uri) +{ + World* world = _lv2_plugin->world(); + LilvWorld* lworld = world->lilv_world(); + LilvNode* preset = lilv_new_uri(lworld, uri.c_str()); + + // Load preset into world if necessary + lilv_world_load_resource(lworld, preset); + + // Load preset from world + LV2_URID_Map* map = &world->uri_map().urid_map_feature()->urid_map; + LilvState* state = lilv_state_new_from_world(lworld, map, preset); + + lilv_node_free(preset); + return state; +} + +LilvState* +LV2Block::load_state(World* world, const FilePath& path) +{ + LilvWorld* lworld = world->lilv_world(); + const URI uri = URI(path); + LilvNode* subject = lilv_new_uri(lworld, uri.c_str()); + + LilvState* state = lilv_state_new_from_file( + lworld, + &world->uri_map().urid_map_feature()->urid_map, + subject, + path.c_str()); + + lilv_node_free(subject); + return state; +} + +void +LV2Block::apply_state(const UPtr<Worker>& worker, const LilvState* state) +{ + World* world = parent_graph()->engine().world(); + SPtr<LV2_Feature> sched; + if (worker) { + sched = worker->schedule_feature()->feature(world, this); + } + + const LV2_Feature* state_features[2] = { nullptr, nullptr }; + if (sched) { + state_features[0] = sched.get(); + } + + for (uint32_t v = 0; v < _polyphony; ++v) { + lilv_state_restore(state, instance(v), nullptr, nullptr, 0, state_features); + } +} + +static const void* +get_port_value(const char* port_symbol, + void* user_data, + uint32_t* size, + uint32_t* type) +{ + LV2Block* const block = (LV2Block*)user_data; + PortImpl* const port = block->port_by_symbol(port_symbol); + + if (port && port->is_input() && port->value().is_valid()) { + *size = port->value().size(); + *type = port->value().type(); + return port->value().get_body(); + } + + return nullptr; +} + +boost::optional<Resource> +LV2Block::save_preset(const URI& uri, + const Properties& props) +{ + World* world = parent_graph()->engine().world(); + LilvWorld* lworld = _lv2_plugin->world()->lilv_world(); + LV2_URID_Map* lmap = &world->uri_map().urid_map_feature()->urid_map; + LV2_URID_Unmap* lunmap = &world->uri_map().urid_unmap_feature()->urid_unmap; + + const FilePath path = FilePath(uri.path()); + const FilePath dirname = path.parent_path(); + const FilePath basename = path.stem(); + + LilvState* state = lilv_state_new_from_instance( + _lv2_plugin->lilv_plugin(), instance(0), lmap, + nullptr, nullptr, nullptr, path.c_str(), + get_port_value, this, LV2_STATE_IS_NATIVE, nullptr); + + if (state) { + const Properties::const_iterator l = props.find(_uris.rdfs_label); + if (l != props.end() && l->second.type() == _uris.atom_String) { + lilv_state_set_label(state, l->second.ptr<char>()); + } + + lilv_state_save(lworld, lmap, lunmap, state, nullptr, + dirname.c_str(), basename.c_str()); + + const URI uri(lilv_node_as_uri(lilv_state_get_uri(state))); + const std::string label(lilv_state_get_label(state) + ? lilv_state_get_label(state) + : basename); + lilv_state_free(state); + + Resource preset(_uris, uri); + preset.set_property(_uris.rdf_type, _uris.pset_Preset); + preset.set_property(_uris.rdfs_label, world->forge().alloc(label)); + preset.set_property(_uris.lv2_appliesTo, + world->forge().make_urid(_lv2_plugin->uri())); + + const std::string bundle_uri = URI(dirname).string() + '/'; + LilvNode* lbundle = lilv_new_uri(lworld, bundle_uri.c_str()); + lilv_world_load_bundle(lworld, lbundle); + lilv_node_free(lbundle); + + return preset; + } + + return boost::optional<Resource>(); +} + +void +LV2Block::set_port_buffer(uint32_t voice, + uint32_t port_num, + BufferRef buf, + SampleCount offset) +{ + BlockImpl::set_port_buffer(voice, port_num, buf, offset); + lilv_instance_connect_port( + instance(voice), + port_num, + buf ? buf->port_data(_ports->at(port_num)->type(), offset) : nullptr); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/LV2Block.hpp b/src/server/LV2Block.hpp new file mode 100644 index 00000000..f3a59550 --- /dev/null +++ b/src/server/LV2Block.hpp @@ -0,0 +1,152 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LV2BLOCK_HPP +#define INGEN_ENGINE_LV2BLOCK_HPP + +#include <mutex> + +#include "lilv/lilv.h" +#include "lv2/lv2plug.in/ns/ext/worker/worker.h" +#include "raul/Maid.hpp" + +#include "BufferRef.hpp" +#include "BlockImpl.hpp" +#include "ingen/LV2Features.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class LV2Plugin; + +/** An instance of a LV2 plugin. + * + * \ingroup engine + */ +class LV2Block : public BlockImpl +{ +public: + LV2Block(LV2Plugin* plugin, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + ~LV2Block(); + + bool instantiate(BufferFactory& bufs, const LilvState* state); + + LilvInstance* instance() { return instance(0); } + bool save_state(const FilePath& dir) const; + + BlockImpl* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent); + + bool prepare_poly(BufferFactory& bufs, uint32_t poly); + bool apply_poly(RunContext& context, uint32_t poly); + + void activate(BufferFactory& bufs); + void deactivate(); + + LV2_Worker_Status work(uint32_t size, const void* data); + + void run(RunContext& context); + void post_process(RunContext& context); + + LilvState* load_preset(const URI& uri); + + void apply_state(const UPtr<Worker>& worker, const LilvState* state); + + boost::optional<Resource> save_preset(const URI& uri, + const Properties& props); + + void set_port_buffer(uint32_t voice, + uint32_t port_num, + BufferRef buf, + SampleCount offset); + + static LilvState* load_state(World* world, const FilePath& path); + +protected: + struct Instance : public Raul::Noncopyable { + explicit Instance(LilvInstance* i) : instance(i) {} + + ~Instance() { lilv_instance_free(instance); } + + LilvInstance* const instance; + }; + + SPtr<Instance> make_instance(URIs& uris, + SampleRate rate, + uint32_t voice, + bool preparing); + + inline LilvInstance* instance(uint32_t voice) { + return (LilvInstance*)(*_instances)[voice]->instance; + } + + typedef Raul::Array< SPtr<Instance> > Instances; + + void drop_instances(const MPtr<Instances>& instances) { + if (instances) { + for (size_t i = 0; i < instances->size(); ++i) { + (*instances)[i].reset(); + } + } + } + + struct Response : public Raul::Maid::Disposable + , public Raul::Noncopyable + , public boost::intrusive::slist_base_hook<> + { + inline Response(uint32_t s, const void* d) + : size(s) + , data(malloc(s)) + { + memcpy(data, d, s); + } + + ~Response() { + free(data); + } + + const uint32_t size; + void* const data; + }; + + typedef boost::intrusive::slist<Response, + boost::intrusive::cache_last<true>, + boost::intrusive::constant_time_size<false> + > Responses; + + static LV2_Worker_Status work_respond( + LV2_Worker_Respond_Handle handle, uint32_t size, const void* data); + + LV2Plugin* _lv2_plugin; + MPtr<Instances> _instances; + MPtr<Instances> _prepared_instances; + const LV2_Worker_Interface* _worker_iface; + std::mutex _work_mutex; + Responses _responses; + SPtr<LV2Features::FeatureArray> _features; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LV2BLOCK_HPP diff --git a/src/server/LV2Options.hpp b/src/server/LV2Options.hpp new file mode 100644 index 00000000..ef7c5ec9 --- /dev/null +++ b/src/server/LV2Options.hpp @@ -0,0 +1,71 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LV2OPTIONS_HPP +#define INGEN_ENGINE_LV2OPTIONS_HPP + +#include "ingen/LV2Features.hpp" +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/options/options.h" + +namespace Ingen { +namespace Server { + +class LV2Options : public Ingen::LV2Features::Feature { +public: + explicit LV2Options(const URIs& uris) + : _uris(uris) + {} + + void set(int32_t sample_rate, int32_t block_length, int32_t seq_size) { + _sample_rate = sample_rate; + _block_length = block_length; + _seq_size = seq_size; + } + + const char* uri() const { return LV2_OPTIONS__options; } + + SPtr<LV2_Feature> feature(World* w, Node* n) { + const LV2_Options_Option options[] = { + { LV2_OPTIONS_INSTANCE, 0, _uris.bufsz_minBlockLength, + sizeof(int32_t), _uris.atom_Int, &_block_length }, + { LV2_OPTIONS_INSTANCE, 0, _uris.bufsz_maxBlockLength, + sizeof(int32_t), _uris.atom_Int, &_block_length }, + { LV2_OPTIONS_INSTANCE, 0, _uris.bufsz_sequenceSize, + sizeof(int32_t), _uris.atom_Int, &_seq_size }, + { LV2_OPTIONS_INSTANCE, 0, _uris.param_sampleRate, + sizeof(int32_t), _uris.atom_Int, &_sample_rate }, + { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr } + }; + + LV2_Feature* f = (LV2_Feature*)malloc(sizeof(LV2_Feature)); + f->URI = LV2_OPTIONS__options; + f->data = malloc(sizeof(options)); + memcpy(f->data, options, sizeof(options)); + return SPtr<LV2_Feature>(f, &free_feature); + } + +private: + const URIs& _uris; + int32_t _sample_rate; + int32_t _block_length; + int32_t _seq_size; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LV2OPTIONS_HPP diff --git a/src/server/LV2Plugin.cpp b/src/server/LV2Plugin.cpp new file mode 100644 index 00000000..f56fd4d7 --- /dev/null +++ b/src/server/LV2Plugin.cpp @@ -0,0 +1,143 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/Forge.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/presets/presets.h" + +#include "Engine.hpp" +#include "LV2Block.hpp" +#include "LV2Plugin.hpp" + +namespace Ingen { +namespace Server { + +LV2Plugin::LV2Plugin(World* world, const LilvPlugin* lplugin) + : PluginImpl(world->uris(), + world->uris().lv2_Plugin.urid, + URI(lilv_node_as_uri(lilv_plugin_get_uri(lplugin)))) + , _world(world) + , _lilv_plugin(lplugin) +{ + set_property(_uris.rdf_type, _uris.lv2_Plugin); + + update_properties(); +} + +void +LV2Plugin::update_properties() +{ + LilvNode* minor = lilv_world_get(_world->lilv_world(), + lilv_plugin_get_uri(_lilv_plugin), + _uris.lv2_minorVersion, + nullptr); + LilvNode* micro = lilv_world_get(_world->lilv_world(), + lilv_plugin_get_uri(_lilv_plugin), + _uris.lv2_microVersion, + nullptr); + + if (lilv_node_is_int(minor) && lilv_node_is_int(micro)) { + set_property(_uris.lv2_minorVersion, + _world->forge().make(lilv_node_as_int(minor))); + set_property(_uris.lv2_microVersion, + _world->forge().make(lilv_node_as_int(micro))); + } + + lilv_node_free(minor); + lilv_node_free(micro); +} + +const Raul::Symbol +LV2Plugin::symbol() const +{ + std::string working = uri(); + if (working.back() == '/') { + working = working.substr(0, working.length() - 1); + } + + while (working.length() > 0) { + size_t last_slash = working.find_last_of("/"); + const std::string symbol = working.substr(last_slash+1); + if ( (symbol[0] >= 'a' && symbol[0] <= 'z') + || (symbol[0] >= 'A' && symbol[0] <= 'Z') ) { + return Raul::Symbol::symbolify(symbol); + } else { + working = working.substr(0, last_slash); + } + } + + return Raul::Symbol("lv2_symbol"); +} + +BlockImpl* +LV2Plugin::instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state) +{ + LV2Block* b = new LV2Block( + this, symbol, polyphonic, parent, engine.sample_rate()); + + if (!b->instantiate(bufs, state)) { + delete b; + return nullptr; + } else { + return b; + } +} + +void +LV2Plugin::load_presets() +{ + const URIs& uris = _world->uris(); + LilvWorld* lworld = _world->lilv_world(); + LilvNodes* presets = lilv_plugin_get_related(_lilv_plugin, uris.pset_Preset); + + if (presets) { + LILV_FOREACH(nodes, i, presets) { + const LilvNode* preset = lilv_nodes_get(presets, i); + lilv_world_load_resource(lworld, preset); + + LilvNodes* labels = lilv_world_find_nodes( + lworld, preset, uris.rdfs_label, nullptr); + if (labels) { + const LilvNode* label = lilv_nodes_get_first(labels); + + _presets.emplace(URI(lilv_node_as_uri(preset)), + lilv_node_as_string(label)); + + lilv_nodes_free(labels); + } else { + _world->log().error( + fmt("Preset <%1%> has no rdfs:label\n") + % lilv_node_as_string(lilv_nodes_get(presets, i))); + } + } + + lilv_nodes_free(presets); + } + + PluginImpl::load_presets(); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/LV2Plugin.hpp b/src/server/LV2Plugin.hpp new file mode 100644 index 00000000..43d0fba9 --- /dev/null +++ b/src/server/LV2Plugin.hpp @@ -0,0 +1,72 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LV2PLUGIN_HPP +#define INGEN_ENGINE_LV2PLUGIN_HPP + +#include <cstdlib> + +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +#include "PluginImpl.hpp" + +namespace Ingen { + +class World; + +namespace Server { + +class GraphImpl; +class BlockImpl; + +/** Implementation of an LV2 plugin (loaded shared library). + */ +class LV2Plugin : public PluginImpl +{ +public: + LV2Plugin(World* world, const LilvPlugin* lplugin); + + BlockImpl* instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state); + + const Raul::Symbol symbol() const; + + World* world() const { return _world; } + const LilvPlugin* lilv_plugin() const { return _lilv_plugin; } + + void update_properties(); + + void load_presets(); + + URI bundle_uri() const { + const LilvNode* bundle = lilv_plugin_get_bundle_uri(_lilv_plugin); + return URI(lilv_node_as_uri(bundle)); + } + +private: + World* _world; + const LilvPlugin* _lilv_plugin; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LV2PLUGIN_HPP diff --git a/src/server/LV2ResizeFeature.hpp b/src/server/LV2ResizeFeature.hpp new file mode 100644 index 00000000..f61165ee --- /dev/null +++ b/src/server/LV2ResizeFeature.hpp @@ -0,0 +1,65 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LV2RESIZEFEATURE_HPP +#define INGEN_ENGINE_LV2RESIZEFEATURE_HPP + +#include "ingen/LV2Features.hpp" +#include "lv2/lv2plug.in/ns/ext/resize-port/resize-port.h" + +#include "BlockImpl.hpp" +#include "Buffer.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +struct ResizeFeature : public Ingen::LV2Features::Feature { + static LV2_Resize_Port_Status resize_port( + LV2_Resize_Port_Feature_Data data, + uint32_t index, + size_t size) { + BlockImpl* block = (BlockImpl*)data; + PortImpl* port = block->port_impl(index); + if (block->context() == Context::ID::MESSAGE) { + port->buffer(0)->resize(size); + port->connect_buffers(); + return LV2_RESIZE_PORT_SUCCESS; + } + return LV2_RESIZE_PORT_ERR_UNKNOWN; + } + + const char* uri() const { return LV2_RESIZE_PORT_URI; } + + SPtr<LV2_Feature> feature(World* w, Node* n) { + BlockImpl* block = dynamic_cast<BlockImpl*>(n); + if (!block) + return SPtr<LV2_Feature>(); + LV2_Resize_Port_Resize* data + = (LV2_Resize_Port_Resize*)malloc(sizeof(LV2_Resize_Port_Resize)); + data->data = block; + data->resize = &resize_port; + LV2_Feature* f = (LV2_Feature*)malloc(sizeof(LV2_Feature)); + f->URI = LV2_RESIZE_PORT_URI; + f->data = data; + return SPtr<LV2_Feature>(f, &free_feature); + } +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LV2RESIZEFEATURE_HPP diff --git a/src/server/Load.hpp b/src/server/Load.hpp new file mode 100644 index 00000000..ed9ee406 --- /dev/null +++ b/src/server/Load.hpp @@ -0,0 +1,57 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LOAD_HPP +#define INGEN_ENGINE_LOAD_HPP + +namespace Ingen { +namespace Server { + +struct Load +{ + void update(uint64_t time, uint64_t available) { + const uint64_t load = time * 100 / available; + if (load < min) { + min = load; + changed = true; + } + if (load > max) { + max = load; + changed = true; + } + if (++n == 1) { + mean = load; + changed = true; + } else { + const float a = mean + ((float)load - mean) / (float)++n; + if (a != mean) { + changed = floorf(a) != floorf(mean); + mean = a; + } + } + } + + uint64_t min = std::numeric_limits<uint64_t>::max(); + uint64_t max = 0; + float mean = 0.0f; + uint64_t n = 0; + bool changed = false; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LOAD_HPP diff --git a/src/server/NodeImpl.cpp b/src/server/NodeImpl.cpp new file mode 100644 index 00000000..778ba15a --- /dev/null +++ b/src/server/NodeImpl.cpp @@ -0,0 +1,50 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "GraphImpl.hpp" +#include "NodeImpl.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +NodeImpl::NodeImpl(const Ingen::URIs& uris, + NodeImpl* parent, + const Raul::Symbol& symbol) + : Node(uris, parent ? parent->path().child(symbol) : Raul::Path("/")) + , _parent(parent) + , _path(parent ? parent->path().child(symbol) : Raul::Path("/")) + , _symbol(symbol) +{ +} + +const Atom& +NodeImpl::get_property(const URI& key) const +{ + ThreadManager::assert_not_thread(THREAD_PROCESS); + static const Atom null_atom; + auto i = properties().find(key); + return (i != properties().end()) ? i->second : null_atom; +} + +GraphImpl* +NodeImpl::parent_graph() const +{ + return dynamic_cast<GraphImpl*>((BlockImpl*)_parent); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/NodeImpl.hpp b/src/server/NodeImpl.hpp new file mode 100644 index 00000000..614801eb --- /dev/null +++ b/src/server/NodeImpl.hpp @@ -0,0 +1,109 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_NODEIMPL_HPP +#define INGEN_ENGINE_NODEIMPL_HPP + +#include <cassert> +#include <cstddef> +#include <map> + +#include "ingen/Node.hpp" +#include "ingen/Resource.hpp" +#include "raul/Deletable.hpp" +#include "raul/Path.hpp" + +namespace Raul { class Maid; } + +namespace Ingen { + +namespace Shared { class URIs; } + +namespace Server { + +class BufferFactory; +class GraphImpl; +class RunContext; + +/** An object on the audio graph (a Graph, Block, or Port). + * + * Each of these is a Raul::Deletable and so can be deleted in a realtime safe + * way from anywhere, and they all have a map of variable for clients to store + * arbitrary values in (which the engine puts no significance to whatsoever). + * + * \ingroup engine + */ +class NodeImpl : public Node +{ +public: + const Raul::Symbol& symbol() const { return _symbol; } + + Node* graph_parent() const { return _parent; } + NodeImpl* parent() const { return _parent; } + + /** Rename */ + virtual void set_path(const Raul::Path& new_path) { + _path = new_path; + const char* const new_sym = new_path.symbol(); + if (new_sym[0] != '\0') { + _symbol = Raul::Symbol(new_sym); + } + set_uri(path_to_uri(new_path)); + } + + const Atom& get_property(const URI& key) const; + + /** The Graph this object is a child of. */ + virtual GraphImpl* parent_graph() const; + + const Raul::Path& path() const { return _path; } + + /** Prepare for a new (external) polyphony value. + * + * Preprocessor thread, poly is actually applied by apply_poly. + * \return true on success. + */ + virtual bool prepare_poly(BufferFactory& bufs, uint32_t poly) = 0; + + /** Apply a new (external) polyphony value. + * + * \param context Process context (process thread only). + * \param poly Must be <= the most recent value passed to prepare_poly. + */ + virtual bool apply_poly(RunContext& context, uint32_t poly) = 0; + + /** Return true iff this is main (the top level Node). + * + * This is sometimes called "the root graph", but the term "main" is used + * to avoid ambiguity with the root path, since main does not have the path + * "/", but usually "/main" to leave namespace for non-node things. + */ + bool is_main() const { return !_parent; } + +protected: + NodeImpl(const Ingen::URIs& uris, + NodeImpl* parent, + const Raul::Symbol& symbol); + + NodeImpl* _parent; + Raul::Path _path; + Raul::Symbol _symbol; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_NODEIMPL_HPP diff --git a/src/server/OutputPort.hpp b/src/server/OutputPort.hpp new file mode 100644 index 00000000..1058defb --- /dev/null +++ b/src/server/OutputPort.hpp @@ -0,0 +1,51 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_OUTPUTPORT_HPP +#define INGEN_ENGINE_OUTPUTPORT_HPP + +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +/** An output port. + * + * Output ports always have a locally allocated buffer, and buffer() will + * always return that buffer. + * + * \ingroup engine + */ +class OutputPort : public PortImpl +{ +public: + OutputPort(BufferFactory& bufs, + BlockImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size = 0) + : PortImpl(bufs, parent, symbol, index,poly, type, buffer_type, value, buffer_size, true) + {} +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_OUTPUTPORT_HPP diff --git a/src/server/PluginImpl.hpp b/src/server/PluginImpl.hpp new file mode 100644 index 00000000..ebd4b3e5 --- /dev/null +++ b/src/server/PluginImpl.hpp @@ -0,0 +1,96 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PLUGINIMPL_HPP +#define INGEN_ENGINE_PLUGINIMPL_HPP + +#include <cstdlib> + +#include "ingen/Resource.hpp" +#include "raul/Symbol.hpp" + +namespace Ingen { + +class URIs; + +namespace Server { + +class BlockImpl; +class BufferFactory; +class Engine; +class GraphImpl; + +/** Implementation of a plugin (internal code, or a loaded shared library). + * + * Conceptually, a Block is an instance of this. + */ +class PluginImpl : public Resource +{ +public: + PluginImpl(Ingen::URIs& uris, const Atom& type, const URI& uri) + : Resource(uris, uri) + , _type(type) + , _presets_loaded(false) + , _is_zombie(false) + { + } + + virtual BlockImpl* instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state) = 0; + + virtual const Raul::Symbol symbol() const = 0; + + const Atom& type() const { return _type; } + void set_type(const Atom& t) { _type = t; } + bool is_zombie() const { return _is_zombie; } + void set_is_zombie(bool t) { _is_zombie = t; } + + typedef std::pair<URI, std::string> Preset; + typedef std::map<URI, std::string> Presets; + + const Presets& presets(bool force_reload=false) { + if (!_presets_loaded || force_reload) { + load_presets(); + } + + return _presets; + } + + virtual void update_properties() {} + + virtual void load_presets() { _presets_loaded = true; } + + virtual URI bundle_uri() const { return URI("ingen:/"); } + +protected: + Atom _type; + Presets _presets; + bool _presets_loaded; + bool _is_zombie; + +private: + PluginImpl(const PluginImpl&) = delete; + PluginImpl& operator=(const PluginImpl&) = delete; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PLUGINIMPL_HPP diff --git a/src/server/PortAudioDriver.cpp b/src/server/PortAudioDriver.cpp new file mode 100644 index 00000000..f892c99f --- /dev/null +++ b/src/server/PortAudioDriver.cpp @@ -0,0 +1,297 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen_config.h" + +#include <cstdlib> +#include <string> + +#include <portaudio.h> + +#include "ingen/Configuration.hpp" +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "Buffer.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PortAudioDriver.hpp" +#include "PortImpl.hpp" +#include "FrameTimer.hpp" +#include "ThreadManager.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { + +static bool +pa_error(const char* msg, PaError err) +{ + fprintf(stderr, "error: %s (%s)\n", msg, Pa_GetErrorText(err)); + Pa_Terminate(); + return false; +} + +PortAudioDriver::PortAudioDriver(Engine& engine) + : _engine(engine) + , _sem(0) + , _stream(nullptr) + , _seq_size(4096) + , _block_length(engine.world()->conf().option("buffer-size").get<int32_t>()) + , _sample_rate(48000) + , _n_inputs(0) + , _n_outputs(0) + , _flag(false) + , _is_activated(false) +{ +} + +PortAudioDriver::~PortAudioDriver() +{ + deactivate(); + _ports.clear_and_dispose([](EnginePort* p) { delete p; }); +} + +bool +PortAudioDriver::attach() +{ + PaError st = paNoError; + if ((st = Pa_Initialize())) { + return pa_error("Failed to initialize audio system", st); + } + + // Get default input and output devices + _inputParameters.device = Pa_GetDefaultInputDevice(); + _outputParameters.device = Pa_GetDefaultOutputDevice(); + if (_inputParameters.device == paNoDevice) { + return pa_error("No default input device", paDeviceUnavailable); + } else if (_outputParameters.device == paNoDevice) { + return pa_error("No default output device", paDeviceUnavailable); + } + + const PaDeviceInfo* in_dev = Pa_GetDeviceInfo(_inputParameters.device); + + /* TODO: It looks like it is somehow actually impossible to request the + best/native buffer size then retrieve what it actually is with + PortAudio. How such a glaring useless flaw exists in such a widespread + library is beyond me... */ + + _sample_rate = in_dev->defaultSampleRate; + + _timer = std::unique_ptr<FrameTimer>( + new FrameTimer(_block_length, _sample_rate)); + + return true; +} + +bool +PortAudioDriver::activate() +{ + const PaDeviceInfo* in_dev = Pa_GetDeviceInfo(_inputParameters.device); + const PaDeviceInfo* out_dev = Pa_GetDeviceInfo(_outputParameters.device); + + // Count number of input and output audio ports/channels + _inputParameters.channelCount = 0; + _outputParameters.channelCount = 0; + for (const auto& port : _ports) { + if (port.graph_port()->is_a(PortType::AUDIO)) { + if (port.graph_port()->is_input()) { + ++_inputParameters.channelCount; + } else if (port.graph_port()->is_output()) { + ++_outputParameters.channelCount; + } + } + } + + // Configure audio format + _inputParameters.sampleFormat = paFloat32|paNonInterleaved; + _inputParameters.suggestedLatency = in_dev->defaultLowInputLatency; + _inputParameters.hostApiSpecificStreamInfo = nullptr; + _outputParameters.sampleFormat = paFloat32|paNonInterleaved; + _outputParameters.suggestedLatency = out_dev->defaultLowOutputLatency; + _outputParameters.hostApiSpecificStreamInfo = nullptr; + + // Open stream + PaError st = paNoError; + if ((st = Pa_OpenStream( + &_stream, + _inputParameters.channelCount ? &_inputParameters : nullptr, + _outputParameters.channelCount ? &_outputParameters : nullptr, + in_dev->defaultSampleRate, + _block_length, // paFramesPerBufferUnspecified, // FIXME: ? + 0, + pa_process_cb, + this))) { + return pa_error("Failed to open audio stream", st); + } + + _is_activated = true; + if ((st = Pa_StartStream(_stream))) { + return pa_error("Error starting audio stream", st); + } + + return true; +} + +void +PortAudioDriver::deactivate() +{ + Pa_Terminate(); +} + +SampleCount +PortAudioDriver::frame_time() const +{ + return _timer->frame_time(_engine.current_time()) + _engine.block_length(); +} + +EnginePort* +PortAudioDriver::get_port(const Raul::Path& path) +{ + for (auto& p : _ports) { + if (p.graph_port()->path() == path) { + return &p; + } + } + + return nullptr; +} + +void +PortAudioDriver::add_port(RunContext& context, EnginePort* port) +{ + _ports.push_back(*port); +} + +void +PortAudioDriver::remove_port(RunContext& context, EnginePort* port) +{ + _ports.erase(_ports.iterator_to(*port)); +} + +void +PortAudioDriver::register_port(EnginePort& port) +{ +} + +void +PortAudioDriver::unregister_port(EnginePort& port) +{ +} + +void +PortAudioDriver::rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) +{ +} + +void +PortAudioDriver::port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) +{ +} + +EnginePort* +PortAudioDriver::create_port(DuplexPort* graph_port) +{ + EnginePort* eport = nullptr; + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + // Audio buffer port, use Jack buffer directly + eport = new EnginePort(graph_port); + graph_port->set_is_driver_port(*_engine.buffer_factory()); + } else if (graph_port->is_a(PortType::ATOM) && + graph_port->buffer_type() == _engine.world()->uris().atom_Sequence) { + // Sequence port, make Jack port but use internal LV2 format buffer + eport = new EnginePort(graph_port); + } + + if (graph_port->is_a(PortType::AUDIO)) { + if (graph_port->is_input()) { + eport->set_driver_index(_n_inputs++); + } else { + eport->set_driver_index(_n_outputs++); + } + } + + if (eport) { + register_port(*eport); + } + + return eport; +} + +void +PortAudioDriver::pre_process_port(RunContext& context, + EnginePort* port, + const void* inputs, + void* outputs) +{ + if (!port->graph_port()->is_a(PortType::AUDIO)) { + return; + } + + if (port->is_input()) { + port->set_buffer(((float**)inputs)[port->driver_index()]); + } else { + port->set_buffer(((float**)outputs)[port->driver_index()]); + memset(port->buffer(), 0, _block_length * sizeof(float)); + } + + port->graph_port()->set_driver_buffer( + port->buffer(), _block_length * sizeof(float)); +} + +void +PortAudioDriver::post_process_port(RunContext& context, + EnginePort* port, + const void* inputs, + void* outputs) +{ +} + +int +PortAudioDriver::process_cb(const void* inputs, + void* outputs, + unsigned long nframes, + const PaStreamCallbackTimeInfo* time, + PaStreamCallbackFlags flags) +{ + _engine.advance(nframes); + _timer->update(_engine.current_time(), _engine.run_context().start()); + + // Read input + for (auto& p : _ports) { + pre_process_port(_engine.run_context(), &p, inputs, outputs); + } + + // Process + _engine.run(nframes); + + // Write output + for (auto& p : _ports) { + post_process_port(_engine.run_context(), &p, inputs, outputs); + } + + return 0; +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/PortAudioDriver.hpp b/src/server/PortAudioDriver.hpp new file mode 100644 index 00000000..b1545f64 --- /dev/null +++ b/src/server/PortAudioDriver.hpp @@ -0,0 +1,132 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PORTAUDIODRIVER_HPP +#define INGEN_ENGINE_PORTAUDIODRIVER_HPP + +#include "ingen_config.h" + +#include <atomic> +#include <memory> +#include <string> + +#include <portaudio.h> + +#include "raul/Semaphore.hpp" + +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" + +#include "Driver.hpp" +#include "EnginePort.hpp" + +namespace Raul { class Path; } + +namespace Ingen { +namespace Server { + +class DuplexPort; +class Engine; +class GraphImpl; +class PortAudioDriver; +class PortImpl; +class FrameTimer; + +class PortAudioDriver : public Driver +{ +public: + explicit PortAudioDriver(Engine& engine); + ~PortAudioDriver(); + + bool attach(); + + bool activate(); + void deactivate(); + + EnginePort* create_port(DuplexPort* graph_port); + EnginePort* get_port(const Raul::Path& path); + + void rename_port(const Raul::Path& old_path, const Raul::Path& new_path); + void port_property(const Raul::Path& path, const URI& uri, const Atom& value); + void add_port(RunContext& context, EnginePort* port); + void remove_port(RunContext& context, EnginePort* port); + void register_port(EnginePort& port); + void unregister_port(EnginePort& port); + + void append_time_events(RunContext& context, Buffer& buffer) {} + + SampleCount frame_time() const; + + int real_time_priority() { return 80; } + + SampleCount block_length() const { return _block_length; } + size_t seq_size() const { return _seq_size; } + SampleCount sample_rate() const { return _sample_rate; } + +private: + friend class PortAudioPort; + + inline static int + pa_process_cb(const void* inputs, + void* outputs, + unsigned long nframes, + const PaStreamCallbackTimeInfo* time, + PaStreamCallbackFlags flags, + void* handle) { + return ((PortAudioDriver*)handle)->process_cb( + inputs, outputs, nframes, time, flags); + } + + int process_cb(const void* inputs, + void* outputs, + unsigned long nframes, + const PaStreamCallbackTimeInfo* time, + PaStreamCallbackFlags flags); + + void pre_process_port(RunContext& context, + EnginePort* port, + const void* inputs, + void* outputs); + + void post_process_port(RunContext& context, + EnginePort* port, + const void* inputs, + void* outputs); + +protected: + typedef boost::intrusive::slist<EnginePort, + boost::intrusive::cache_last<true> + > Ports; + + Engine& _engine; + Ports _ports; + PaStreamParameters _inputParameters; + PaStreamParameters _outputParameters; + Raul::Semaphore _sem; + std::unique_ptr<FrameTimer> _timer; + PaStream* _stream; + size_t _seq_size; + uint32_t _block_length; + uint32_t _sample_rate; + uint32_t _n_inputs; + uint32_t _n_outputs; + std::atomic<bool> _flag; + bool _is_activated; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PORTAUDIODRIVER_HPP diff --git a/src/server/PortImpl.cpp b/src/server/PortImpl.cpp new file mode 100644 index 00000000..b0ef3c85 --- /dev/null +++ b/src/server/PortImpl.cpp @@ -0,0 +1,569 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "raul/Array.hpp" +#include "raul/Maid.hpp" + +#include "BlockImpl.hpp" +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "PortImpl.hpp" +#include "PortType.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +static const uint32_t monitor_rate = 25.0; // Hz + +/** The length of time between monitor updates in frames */ +static inline uint32_t +monitor_period(const Engine& engine) +{ + return std::max(engine.block_length(), + engine.sample_rate() / monitor_rate); +} + +PortImpl::PortImpl(BufferFactory& bufs, + BlockImpl* const block, + const Raul::Symbol& name, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size, + bool is_output) + : NodeImpl(bufs.uris(), block, name) + , _bufs(bufs) + , _index(index) + , _poly(poly) + , _buffer_size(buffer_size) + , _frames_since_monitor(0) + , _monitor_value(0.0f) + , _peak(0.0f) + , _type(type) + , _buffer_type(buffer_type) + , _value(value) + , _min(bufs.forge().make(0.0f)) + , _max(bufs.forge().make(1.0f)) + , _voices(bufs.maid().make_managed<Voices>(poly)) + , _connected_flag(false) + , _monitored(false) + , _force_monitor_update(false) + , _is_morph(false) + , _is_auto_morph(false) + , _is_logarithmic(false) + , _is_sample_rate(false) + , _is_toggled(false) + , _is_driver_port(false) + , _is_output(is_output) +{ + assert(block != nullptr); + assert(_poly > 0); + + const Ingen::URIs& uris = bufs.uris(); + + set_type(type, buffer_type); + + remove_property(uris.lv2_index, uris.patch_wildcard); + set_property(uris.lv2_index, bufs.forge().make((int32_t)index)); + + if (has_value()) { + set_property(uris.ingen_value, value); + } + if (type == PortType::ATOM) { + set_property(uris.atom_bufferType, + bufs.forge().make_urid(buffer_type)); + } + + if (is_output) { + if (_parent->graph_type() != Node::GraphType::GRAPH) { + add_property(bufs.uris().rdf_type, bufs.uris().lv2_OutputPort.urid); + } + } + + get_buffers(bufs, &BufferFactory::get_buffer, _voices, poly, 0); +} + +bool +PortImpl::get_buffers(BufferFactory& bufs, + GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const +{ + for (uint32_t v = 0; v < poly; ++v) { + voices->at(v).buffer.reset(); + voices->at(v).buffer = (bufs.*get)( + buffer_type(), _value.type(), _buffer_size); + } + + return true; +} + +bool +PortImpl::setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly) +{ + return get_buffers(bufs, &BufferFactory::claim_buffer, _voices, poly, 0); +} + +void +PortImpl::set_type(PortType port_type, LV2_URID buffer_type) +{ + const Ingen::URIs& uris = _bufs.uris(); + Ingen::World* world = _bufs.engine().world(); + + // Update type properties so clients are aware of current type + remove_property(uris.rdf_type, uris.lv2_AudioPort); + remove_property(uris.rdf_type, uris.lv2_CVPort); + remove_property(uris.rdf_type, uris.lv2_ControlPort); + remove_property(uris.rdf_type, uris.atom_AtomPort); + add_property(uris.rdf_type, world->forge().make_urid(port_type.uri())); + + // Update audio thread types + _type = port_type; + _buffer_type = buffer_type; + if (!_buffer_type) { + switch (_type.id()) { + case PortType::CONTROL: + _buffer_type = uris.atom_Float; + break; + case PortType::AUDIO: + case PortType::CV: + _buffer_type = uris.atom_Sound; + break; + default: + break; + } + } + _buffer_size = std::max(_buffer_size, _bufs.default_size(_buffer_type)); +} + +bool +PortImpl::has_value() const +{ + return (_type == PortType::CONTROL || + _type == PortType::CV || + (_type == PortType::ATOM && + _value.type() == _bufs.uris().atom_Float)); +} + +bool +PortImpl::supports(const URIs::Quark& value_type) const +{ + return has_property(_bufs.uris().atom_supports, value_type); +} + +void +PortImpl::activate(BufferFactory& bufs) +{ + /* Set the time since the last monitor update to a random value within the + monitor period, to spread the load out over time. Otherwise, every + port would try to send an update at exactly the same time, every time. + */ + const double srate = bufs.engine().sample_rate(); + const uint32_t period = srate / monitor_rate; + _frames_since_monitor = bufs.engine().frand() * period; + _monitor_value = 0.0f; + _peak = 0.0f; + + // Trigger buffer re-connect next cycle + _connected_flag.clear(std::memory_order_release); +} + +void +PortImpl::deactivate() +{ + if (is_output() && !_is_driver_port) { + for (uint32_t v = 0; v < _poly; ++v) { + if (_voices->at(v).buffer) { + _voices->at(v).buffer->clear(); + } + } + } + _monitor_value = 0.0f; + _peak = 0.0f; +} + +void +PortImpl::set_voices(RunContext& context, MPtr<Voices>&& voices) +{ + _voices = std::move(voices); + connect_buffers(); +} + +void +PortImpl::cache_properties() +{ + _is_logarithmic = has_property(_bufs.uris().lv2_portProperty, + _bufs.uris().pprops_logarithmic); + _is_sample_rate = has_property(_bufs.uris().lv2_portProperty, + _bufs.uris().lv2_sampleRate); + _is_toggled = has_property(_bufs.uris().lv2_portProperty, + _bufs.uris().lv2_toggled); +} + +void +PortImpl::set_control_value(const RunContext& context, + FrameTime time, + Sample value) +{ + for (uint32_t v = 0; v < _poly; ++v) { + update_set_state(context, v); + set_voice_value(context, v, time, value); + } +} + +void +PortImpl::set_voice_value(const RunContext& context, + uint32_t voice, + FrameTime time, + Sample value) +{ + switch (_type.id()) { + case PortType::CONTROL: + if (buffer(voice)->value()) { + ((LV2_Atom_Float*)buffer(voice)->value())->body = value; + } + _voices->at(voice).set_state.set(context, context.start(), value); + break; + case PortType::AUDIO: + case PortType::CV: { + // Time may be at end so internal blocks can set triggers + assert(time >= context.start()); + assert(time <= context.start() + context.nframes()); + + const FrameTime offset = time - context.start(); + if (offset < context.nframes()) { + buffer(voice)->set_block(value, offset, context.nframes()); + } + /* else, this is a set at context.nframes(), used to reset a CV port's + value for the next block, particularly for triggers on the last + frame of a block (set nframes-1 to 1, then nframes to 0). */ + + _voices->at(voice).set_state.set(context, time, value); + } break; + case PortType::ATOM: + if (buffer(voice)->is_sequence()) { + const FrameTime offset = time - context.start(); + // Same deal as above + if (offset < context.nframes()) { + buffer(voice)->append_event(offset, + sizeof(value), + _bufs.uris().atom_Float, + (const uint8_t*)&value); + } + _voices->at(voice).set_state.set(context, time, value); + } else { +#ifndef NDEBUG + fprintf(stderr, + "error: %s set non-sequence atom port value (buffer type %u)\n", + path().c_str(), buffer(voice)->type()); +#endif + } + default: + break; + } +} + +void +PortImpl::update_set_state(const RunContext& context, uint32_t v) +{ + Voice& voice = _voices->at(v); + SetState& state = voice.set_state; + BufferRef buf = voice.buffer; + switch (state.state) { + case SetState::State::SET: + break; + case SetState::State::SET_CYCLE_1: + if (state.time < context.start() && + buf->is_sequence() && + buf->value_type() == _bufs.uris().atom_Float && + !_parent->is_main()) { + buf->clear(); + state.time = context.start(); + } + state.state = SetState::State::SET; + break; + case SetState::State::HALF_SET_CYCLE_1: + state.state = SetState::State::HALF_SET_CYCLE_2; + break; + case SetState::State::HALF_SET_CYCLE_2: + if (buf->is_sequence()) { + buf->clear(); + buf->append_event( + 0, sizeof(float), _bufs.uris().atom_Float, + (const uint8_t*)&state.value); + } else { + buf->set_block(state.value, 0, context.nframes()); + } + state.state = SetState::State::SET_CYCLE_1; + break; + } +} + +bool +PortImpl::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + if (_is_driver_port || _parent->is_main() || + (_type == PortType::ATOM && !_value.is_valid())) { + return false; + } else if (_poly == poly) { + return true; + } else if (_prepared_voices && _prepared_voices->size() != poly) { + _prepared_voices.reset(); + } + + if (!_prepared_voices) { + _prepared_voices = bufs.maid().make_managed<Voices>( + poly, *_voices, Voice()); + } + + get_buffers(bufs, &BufferFactory::get_buffer, + _prepared_voices, _prepared_voices->size(), num_arcs()); + + return true; +} + +bool +PortImpl::apply_poly(RunContext& context, uint32_t poly) +{ + if (_parent->is_main() || + (_type == PortType::ATOM && !_value.is_valid())) { + return false; + } else if (!_prepared_voices) { + return true; + } + + assert(poly == _prepared_voices->size()); + + _poly = poly; + + // Apply a new set of voices from a preceding call to prepare_poly + _voices = std::move(_prepared_voices); + + if (is_a(PortType::CONTROL) || is_a(PortType::CV)) { + set_control_value(context, context.start(), _value.get<float>()); + } + + assert(_voices->size() >= poly); + assert(this->poly() == poly); + assert(!_prepared_voices); + + connect_buffers(); + + return true; +} + +void +PortImpl::set_buffer_size(RunContext& context, BufferFactory& bufs, size_t size) +{ + _buffer_size = size; + + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer->resize(size); + } + + connect_buffers(); +} + +void +PortImpl::connect_buffers(SampleCount offset) +{ + for (uint32_t v = 0; v < _poly; ++v) { + PortImpl::parent_block()->set_port_buffer(v, _index, buffer(v), offset); + } +} + +void +PortImpl::recycle_buffers() +{ + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer = nullptr; + } +} + +void +PortImpl::set_is_driver_port(BufferFactory& bufs) +{ + _is_driver_port = true; +} + +void +PortImpl::clear_buffers(const RunContext& ctx) +{ + switch (_type.id()) { + case PortType::AUDIO: + default: + for (uint32_t v = 0; v < _poly; ++v) { + buffer(v)->clear(); + } + break; + case PortType::CONTROL: + for (uint32_t v = 0; v < _poly; ++v) { + buffer(v)->clear(); + _voices->at(v).set_state.set(ctx, ctx.start(), _value.get<float>()); + } + break; + case PortType::CV: + for (uint32_t v = 0; v < _poly; ++v) { + buffer(v)->set_block(_value.get<float>(), 0, ctx.nframes()); + _voices->at(v).set_state.set(ctx, ctx.start(), _value.get<float>()); + } + break; + } +} + +void +PortImpl::monitor(RunContext& context, bool send_now) +{ + if (!context.must_notify(this)) { + return; + } + + const uint32_t period = monitor_period(context.engine()); + _frames_since_monitor += context.nframes(); + + const bool time_to_send = send_now || _frames_since_monitor >= period; + const bool is_sequence = (_type.id() == PortType::ATOM && + _buffer_type == _bufs.uris().atom_Sequence); + if (!time_to_send && !(is_sequence && _monitored) && (!is_sequence && buffer(0)->value())) { + return; + } + + Forge& forge = context.engine().world()->forge(); + URIs& uris = context.engine().world()->uris(); + LV2_URID key = 0; + float val = 0.0f; + switch (_type.id()) { + case PortType::UNKNOWN: + break; + case PortType::AUDIO: + key = uris.ingen_activity; + val = _peak = std::max(_peak, buffer(0)->peak(context)); + break; + case PortType::CONTROL: + case PortType::CV: + key = uris.ingen_value; + val = buffer(0)->value_at(0); + break; + case PortType::ATOM: + if (_buffer_type == _bufs.uris().atom_Sequence) { + const LV2_Atom* atom = buffer(0)->get<const LV2_Atom>(); + const LV2_Atom* value = buffer(0)->value(); + if (atom->type != _bufs.uris().atom_Sequence) { + /* Buffer contents are not actually a Sequence. Probably an + uninitialized Chunk, so do nothing. */ + } else if (_monitored) { + /* Sequence explicitly monitored, send everything. */ + const LV2_Atom_Sequence* seq = (const LV2_Atom_Sequence*)atom; + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + context.notify(uris.ingen_activity, + context.start() + ev->time.frames, + this, + ev->body.size, + ev->body.type, + LV2_ATOM_BODY(&ev->body)); + } + } else if (value && value->type == _bufs.uris().atom_Float) { + /* Float sequence, monitor as a control. */ + key = uris.ingen_value; + val = ((LV2_Atom_Float*)buffer(0)->value())->body; + } else if (atom->size > sizeof(LV2_Atom_Sequence_Body)) { + /* General sequence, send activity for blinkenlights. */ + const int32_t one = 1; + context.notify(uris.ingen_activity, + context.start(), + this, + sizeof(int32_t), + (LV2_URID)uris.atom_Bool, + &one); + _force_monitor_update = false; + } + } + } + + _frames_since_monitor = _frames_since_monitor % period; + if (key && val != _monitor_value) { + if (context.notify(key, context.start(), this, + sizeof(float), forge.Float, &val)) { + /* Update frames since last update to conceptually zero, but keep + the remainder to preserve load balancing. */ + _frames_since_monitor = _frames_since_monitor % period; + _peak = 0.0f; + _monitor_value = val; + } + // Otherwise failure, leave old value and try again next time + } +} + +BufferRef +PortImpl::value_buffer(uint32_t voice) +{ + return buffer(voice)->value_buffer(); +} + +SampleCount +PortImpl::next_value_offset(SampleCount offset, SampleCount end) const +{ + SampleCount earliest = end; + for (uint32_t v = 0; v < _poly; ++v) { + const SampleCount o = _voices->at(v).buffer->next_value_offset(offset, end); + if (o < earliest) { + earliest = o; + } + } + return earliest; +} + +void +PortImpl::update_values(SampleCount offset, uint32_t voice) +{ + buffer(voice)->update_value_buffer(offset); +} + +void +PortImpl::pre_process(RunContext& context) +{ + if (!_connected_flag.test_and_set(std::memory_order_acquire)) { + connect_buffers(); + clear_buffers(context); + } + + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer->prepare_output_write(context); + } +} + +void +PortImpl::post_process(RunContext& context) +{ + for (uint32_t v = 0; v < _poly; ++v) { + update_set_state(context, v); + update_values(0, v); + } + + monitor(context); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/PortImpl.hpp b/src/server/PortImpl.hpp new file mode 100644 index 00000000..70d90d0a --- /dev/null +++ b/src/server/PortImpl.hpp @@ -0,0 +1,312 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PORTIMPL_HPP +#define INGEN_ENGINE_PORTIMPL_HPP + +#include <cstdlib> + +#include "ingen/Atom.hpp" +#include "raul/Array.hpp" + +#include "BufferRef.hpp" +#include "NodeImpl.hpp" +#include "PortType.hpp" +#include "RunContext.hpp" +#include "types.hpp" + +namespace Raul { class Maid; } + +namespace Ingen { +namespace Server { + +class BlockImpl; +class BufferFactory; +class RunContext; + +/** A port (input or output) on a Block. + * + * The base implementation here is general and/or for output ports (which are + * simplest), InputPort and DuplexPort override functions to provide + * specialized behaviour where necessary. + * + * \ingroup engine + */ +class PortImpl : public NodeImpl +{ +public: + struct SetState { + enum class State { + /// Partially set, first cycle: AAAAA => AAABB. + HALF_SET_CYCLE_1, + + /// Partially set, second cycle: AAABB => BBBBB. + HALF_SET_CYCLE_2, + + /// Fully set, first cycle (clear events if necessary). + SET_CYCLE_1, + + /// Fully set, second cycle and onwards (done). + SET + }; + + SetState() : state(State::SET), value(0), time(0) {} + + void set(const RunContext& context, FrameTime t, Sample v) { + time = t; + value = v; + state = (time == context.start() + ? State::SET + : State::HALF_SET_CYCLE_1); + } + + State state; ///< State of buffer for setting control value + Sample value; ///< Value currently being set + FrameTime time; ///< Time value was set + }; + + struct Voice { + Voice() : buffer(nullptr) {} + + SetState set_state; + BufferRef buffer; + }; + + typedef Raul::Array<Voice> Voices; + + PortImpl(BufferFactory& bufs, + BlockImpl* block, + const Raul::Symbol& name, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size = 0, + bool is_output = true); + + virtual GraphType graph_type() const { return GraphType::PORT; } + + /** A port's parent is always a block, so static cast should be safe */ + BlockImpl* parent_block() const { return (BlockImpl*)_parent; } + + /** Set the the voices (buffers) for this port in the audio thread. */ + void set_voices(RunContext& context, MPtr<Voices>&& voices); + + /** Prepare for a new (external) polyphony value. + * + * Preprocessor thread, poly is actually applied by apply_poly. + */ + virtual bool prepare_poly(BufferFactory& bufs, uint32_t poly); + + /** Apply a new polyphony value. + * + * Audio thread. + * \a poly Must be < the most recent value passed to prepare_poly. + */ + virtual bool apply_poly(RunContext& context, uint32_t poly); + + /** Return the number of arcs (pre-process thraed). */ + virtual size_t num_arcs() const { return 0; } + + const Atom& value() const { return _value; } + void set_value(const Atom& v) { _value = v; } + + const Atom& minimum() const { return _min; } + const Atom& maximum() const { return _max; } + + /* The following two methods store the range in variables so it can be + accessed in the process thread, which is required for applying control + bindings from incoming MIDI data. + */ + void set_minimum(const Atom& min) { _min.set_rt(min); } + void set_maximum(const Atom& max) { _max.set_rt(max); } + + inline BufferRef buffer(uint32_t voice) const { + return _voices->at((_poly == 1) ? 0 : voice).buffer; + } + inline BufferRef prepared_buffer(uint32_t voice) const { + return _prepared_voices->at(voice).buffer; + } + + void update_set_state(const RunContext& context, uint32_t v); + + void set_voice_value(const RunContext& context, + uint32_t voice, + FrameTime time, + Sample value); + + void set_control_value(const RunContext& context, + FrameTime time, + Sample value); + + /** Prepare this port to use an external driver-provided buffer. + * + * This will avoid allocating a buffer for the port, instead the driver + * buffer is used directly. This only makes sense for ports on the + * top-level graph, which are monophonic. Non-real-time, must be called + * before using the port, followed by a call to set_driver_buffer() in the + * processing thread. + */ + virtual void set_is_driver_port(BufferFactory& bufs); + + bool is_driver_port() const { return _is_driver_port; } + + /** Called once per process cycle */ + virtual void pre_process(RunContext& context); + virtual void pre_run(RunContext& context) {} + virtual void post_process(RunContext& context); + + /** Clear/silence all buffers */ + virtual void clear_buffers(const RunContext& ctx); + + /** Claim and apply buffers in the real-time thread. */ + virtual bool setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly); + + void activate(BufferFactory& bufs); + void deactivate(); + + /** + Inherit any properties from a connected neighbour. + + This is used for Graph ports, so e.g. a control input has the range of + all the ports it is connected to. + */ + virtual void inherit_neighbour(const PortImpl* port, + Properties& remove, + Properties& add) {} + + virtual void connect_buffers(SampleCount offset=0); + virtual void recycle_buffers(); + + uint32_t index() const { return _index; } + void set_index(RunContext&, uint32_t index) { _index = index; } + + inline bool is_a(PortType type) const { return _type == type; } + + bool has_value() const; + + PortType type() const { return _type; } + LV2_URID value_type() const { return _value.is_valid() ? _value.type() : 0; } + LV2_URID buffer_type() const { return _buffer_type; } + + bool supports(const URIs::Quark& value_type) const; + + size_t buffer_size() const { return _buffer_size; } + + uint32_t poly() const { + return _poly; + } + uint32_t prepared_poly() const { + return (_prepared_voices) ? _prepared_voices->size() : 1; + } + + void set_buffer_size(RunContext& context, BufferFactory& bufs, size_t size); + + /** Return true iff this port is explicitly monitored. + * + * This is used for plugin UIs which require monitoring for particular + * ports, even if the Ingen client has not requested broadcasting in + * general (e.g. for canvas animation). + */ + bool is_monitored() const { return _monitored; } + + /** Explicitly turn on monitoring for this port. */ + void enable_monitoring(bool monitored) { _monitored = monitored; } + + /** Monitor port value and broadcast to clients periodically. */ + void monitor(RunContext& context, bool send_now=false); + + BufferFactory& bufs() const { return _bufs; } + + BufferRef value_buffer(uint32_t voice); + + BufferRef user_buffer(RunContext&) const { return _user_buffer; } + void set_user_buffer(RunContext&, BufferRef b) { _user_buffer = b; } + + /** Return offset of the first value change after `offset`. */ + virtual SampleCount next_value_offset(SampleCount offset, + SampleCount end) const; + + /** Update value buffer for `voice` to be current as of `offset`. */ + void update_values(SampleCount offset, uint32_t voice); + + void force_monitor_update() { _force_monitor_update = true; } + + void set_morphable(bool is_morph, bool is_auto_morph) { + _is_morph = is_morph; + _is_auto_morph = is_auto_morph; + } + + void set_type(PortType port_type, LV2_URID buffer_type); + + void cache_properties(); + + bool is_input() const { return !_is_output; } + bool is_output() const { return _is_output; } + bool is_morph() const { return _is_morph; } + bool is_auto_morph() const { return _is_auto_morph; } + bool is_logarithmic() const { return _is_logarithmic; } + bool is_sample_rate() const { return _is_sample_rate; } + bool is_toggled() const { return _is_toggled; } + +protected: + typedef BufferRef (BufferFactory::*GetFn)(LV2_URID, LV2_URID, uint32_t); + + /** Set `voices` as the buffers to be used for this port. + * + * This is real-time safe only if `get` is as well, use in the real-time + * thread should pass &BufferFactory::claim_buffer. + * + * @return true iff buffers are locally owned by the port + */ + virtual bool get_buffers(BufferFactory& bufs, + GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const; + + BufferFactory& _bufs; + uint32_t _index; + uint32_t _poly; + uint32_t _buffer_size; + uint32_t _frames_since_monitor; + float _monitor_value; + float _peak; + PortType _type; + LV2_URID _buffer_type; + Atom _value; + Atom _min; + Atom _max; + MPtr<Voices> _voices; + MPtr<Voices> _prepared_voices; + BufferRef _user_buffer; + std::atomic_flag _connected_flag; + bool _monitored; + bool _force_monitor_update; + bool _is_morph; + bool _is_auto_morph; + bool _is_logarithmic; + bool _is_sample_rate; + bool _is_toggled; + bool _is_driver_port; + bool _is_output; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PORTIMPL_HPP diff --git a/src/server/PortType.hpp b/src/server/PortType.hpp new file mode 100644 index 00000000..0b62c5ab --- /dev/null +++ b/src/server/PortType.hpp @@ -0,0 +1,91 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERFACE_PORTTYPE_HPP +#define INGEN_INTERFACE_PORTTYPE_HPP + +#include <cassert> + +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +namespace Ingen { + +/** The type of a port. + * + * This type refers to the type of the port itself (not necessarily the type + * of its contents). Ports with different types can contain the same type of + * data, but may e.g. have different access semantics. + */ +class PortType { +public: + enum ID { + UNKNOWN = 0, + AUDIO = 1, + CONTROL = 2, + CV = 3, + ATOM = 4 + }; + + explicit PortType(const URI& uri) + : _id(UNKNOWN) + { + if (uri == type_uri(AUDIO)) { + _id = AUDIO; + } else if (uri == type_uri(CONTROL)) { + _id = CONTROL; + } else if (uri == type_uri(CV)) { + _id = CV; + } else if (uri == type_uri(ATOM)) { + _id = ATOM; + } + } + + PortType(ID id) : _id(id) {} + + inline const URI& uri() const { return type_uri(_id); } + inline ID id() const { return _id; } + + inline bool operator==(const ID& id) const { return (_id == id); } + inline bool operator!=(const ID& id) const { return (_id != id); } + inline bool operator==(const PortType& type) const { return (_id == type._id); } + inline bool operator!=(const PortType& type) const { return (_id != type._id); } + inline bool operator<(const PortType& type) const { return (_id < type._id); } + + inline bool is_audio() { return _id == AUDIO; } + inline bool is_control() { return _id == CONTROL; } + inline bool is_cv() { return _id == CV; } + inline bool is_atom() { return _id == ATOM; } + +private: + static inline const URI& type_uri(unsigned id_num) { + assert(id_num <= ATOM); + static const URI uris[] = { + URI("http://www.w3.org/2002/07/owl#Nothing"), + URI(LV2_CORE__AudioPort), + URI(LV2_CORE__ControlPort), + URI(LV2_CORE__CVPort), + URI(LV2_ATOM__AtomPort) + }; + return uris[id_num]; + } + + ID _id; +}; + +} // namespace Ingen + +#endif // INGEN_INTERFACE_PORTTYPE_HPP diff --git a/src/server/PostProcessor.cpp b/src/server/PostProcessor.cpp new file mode 100644 index 00000000..b275c36a --- /dev/null +++ b/src/server/PostProcessor.cpp @@ -0,0 +1,114 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> + +#include "Engine.hpp" +#include "Event.hpp" +#include "PostProcessor.hpp" +#include "RunContext.hpp" + +namespace Ingen { +namespace Server { + +class Sentinel : public Event { +public: + Sentinel(Engine& engine) : Event(engine) {} + + bool pre_process(PreProcessContext& ctx) { return false; } + void execute(RunContext& context) {} + void post_process() {} +}; + +PostProcessor::PostProcessor(Engine& engine) + : _engine(engine) + , _head(new Sentinel(engine)) + , _tail(_head.load()) + , _max_time(0) +{ +} + +PostProcessor::~PostProcessor() +{ + /* Delete any straggler events (usually at least one since the event list + is never completely emptied by process()). */ + Event* e = _head; + while (e) { + Event* const next = e->next(); + delete e; + e = next; + } +} + +void +PostProcessor::append(RunContext& context, Event* first, Event* last) +{ + assert(first); + assert(last); + assert(!last->next()); + + // The only place where _tail is written or next links are changed + _tail.load()->next(first); + _tail.store(last); +} + +bool +PostProcessor::pending() const +{ + return _head.load()->next() || _engine.pending_notifications(); +} + +void +PostProcessor::process() +{ + const FrameTime end_time = _max_time; + + /* We can never empty the list and set _head = _tail = NULL since this + would cause a race with append. Instead, head is an already + post-processed node, or initially a sentinel. */ + Event* ev = _head.load(); + Event* next = ev->next(); + if (!next || next->time() >= end_time) { + // Process audio thread notifications until end + _engine.emit_notifications(end_time); + return; + } + + do { + // Delete previously post-processed ev and move to next + delete ev; + ev = next; + + // Process audio thread notifications up until this event's time + _engine.emit_notifications(ev->time()); + + // Post-process event + ev->post_process(); + next = ev->next(); // [1] (see below) + } while (next && next->time() < end_time); + + /* Reached the tail (as far as we're concerned). There may be successors + by now if append() has been called since [1], but that's fine. Now, ev + points to the last post-processed event, which will be the new head. */ + assert(ev); + _head = ev; + + // Process remaining audio thread notifications until end + _engine.emit_notifications(end_time); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/PostProcessor.hpp b/src/server/PostProcessor.hpp new file mode 100644 index 00000000..5a3ffa62 --- /dev/null +++ b/src/server/PostProcessor.hpp @@ -0,0 +1,74 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_POSTPROCESSOR_HPP +#define INGEN_ENGINE_POSTPROCESSOR_HPP + +#include <atomic> + +#include "ingen/ingen.h" + +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class Event; +class RunContext; + +/** Processor for Events after leaving the audio thread. + * + * The audio thread pushes events to this when it is done with them (which + * is realtime-safe), which signals the processing thread through a semaphore + * to handle the event and pass it on to the Maid. + * + * Update: This is all run from main_iteration now to solve scripting + * thread issues. Not sure if this is permanent/ideal or not... + * + * \ingroup engine + */ +class INGEN_API PostProcessor +{ +public: + explicit PostProcessor(Engine& engine); + ~PostProcessor(); + + /** Push a list of events on to the process queue. + realtime-safe, not thread-safe. + */ + void append(RunContext& context, Event* first, Event* last); + + /** Post-process and delete all pending events */ + void process(); + + /** Return true iff any events are pending */ + bool pending() const; + + /** Set the latest event time that should be post-processed */ + void set_end_time(FrameTime time) { _max_time = time; } + +private: + Engine& _engine; + std::atomic<Event*> _head; + std::atomic<Event*> _tail; + std::atomic<FrameTime> _max_time; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_POSTPROCESSOR_HPP diff --git a/src/server/PreProcessContext.hpp b/src/server/PreProcessContext.hpp new file mode 100644 index 00000000..1b57c013 --- /dev/null +++ b/src/server/PreProcessContext.hpp @@ -0,0 +1,84 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PREPROCESSCONTEXT_HPP +#define INGEN_ENGINE_PREPROCESSCONTEXT_HPP + +#include <unordered_set> + +#include "GraphImpl.hpp" + +namespace Raul { class Maid; } + +namespace Ingen { +namespace Server { + +/** Event pre-processing context. + * + * \ingroup engine + */ +class PreProcessContext +{ +public: + typedef std::unordered_set<GraphImpl*> DirtyGraphs; + + /** Return true iff an atomic bundle is currently being pre-processed. */ + bool in_bundle() const { return _in_bundle; } + + /** Set/unset atomic bundle flag. */ + void set_in_bundle(bool b) { _in_bundle = b; } + + /** Return true iff graph should be compiled now (after a change). + * + * This may return false when an atomic bundle is deferring compilation, in + * which case the graph is flagged as dirty for later compilation. + */ + bool must_compile(GraphImpl& graph) { + if (!graph.enabled()) { + return false; + } else if (_in_bundle) { + _dirty_graphs.insert(&graph); + return false; + } else { + return true; + } + } + + /** Compile graph and return the result if necessary. + * + * This may return null when an atomic bundle is deferring compilation, in + * which case the graph is flagged as dirty for later compilation. + */ + MPtr<CompiledGraph> maybe_compile(Raul::Maid& maid, GraphImpl& graph) { + if (must_compile(graph)) { + return compile(maid, graph); + } + return MPtr<CompiledGraph>(); + } + + /** Return all graphs that require compilation after an atomic bundle. */ + const DirtyGraphs& dirty_graphs() const { return _dirty_graphs; } + DirtyGraphs& dirty_graphs() { return _dirty_graphs; } + +private: + DirtyGraphs _dirty_graphs; + bool _in_bundle = false; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PREPROCESSCONTEXT_HPP diff --git a/src/server/PreProcessor.cpp b/src/server/PreProcessor.cpp new file mode 100644 index 00000000..f674284e --- /dev/null +++ b/src/server/PreProcessor.cpp @@ -0,0 +1,248 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <stdexcept> + +#include "ingen/AtomSink.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/World.hpp" + +#include "Engine.hpp" +#include "Event.hpp" +#include "PostProcessor.hpp" +#include "PreProcessContext.hpp" +#include "PreProcessor.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" +#include "UndoStack.hpp" + +namespace Ingen { +namespace Server { + +PreProcessor::PreProcessor(Engine& engine) + : _engine(engine) + , _sem(0) + , _head(nullptr) + , _tail(nullptr) + , _block_state(BlockState::UNBLOCKED) + , _exit_flag(false) + , _thread(&PreProcessor::run, this) +{} + +PreProcessor::~PreProcessor() +{ + if (_thread.joinable()) { + _exit_flag = true; + _sem.post(); + _thread.join(); + } +} + +void +PreProcessor::event(Event* const ev, Event::Mode mode) +{ + // TODO: Probably possible to make this lock-free with CAS + ThreadManager::assert_not_thread(THREAD_IS_REAL_TIME); + std::lock_guard<std::mutex> lock(_mutex); + + assert(!ev->is_prepared()); + assert(!ev->next()); + ev->set_mode(mode); + + /* Note that tail is only used here, not in process(). The head must be + checked first here, since if it is NULL the tail pointer is junk. */ + Event* const head = _head.load(); + if (!head) { + _head = ev; + _tail = ev; + } else { + _tail.load()->next(ev); + _tail = ev; + } + + _sem.post(); +} + +unsigned +PreProcessor::process(RunContext& context, PostProcessor& dest, size_t limit) +{ + Event* const head = _head.load(); + size_t n_processed = 0; + Event* ev = head; + Event* last = ev; + while (ev && ev->is_prepared()) { + switch (_block_state.load()) { + case BlockState::UNBLOCKED: + break; + case BlockState::PRE_BLOCKED: + if (ev->get_execution() == Event::Execution::BLOCK) { + _block_state = BlockState::BLOCKED; + } else if (ev->get_execution() == Event::Execution::ATOMIC) { + _block_state = BlockState::PROCESSING; + } + break; + case BlockState::BLOCKED: + break; + case BlockState::PRE_UNBLOCKED: + assert(ev->get_execution() == Event::Execution::BLOCK); + if (ev->get_execution() == Event::Execution::BLOCK) { + _block_state = BlockState::PROCESSING; + } + break; + case BlockState::PROCESSING: + if (ev->get_execution() == Event::Execution::UNBLOCK) { + _block_state = BlockState::UNBLOCKED; + } + } + + if (_block_state == BlockState::BLOCKED) { + break; // Waiting for PRE_UNBLOCKED + } else if (ev->time() < context.start()) { + ev->set_time(context.start()); // Too late, nudge to context start + } else if (_block_state != BlockState::PROCESSING && + ev->time() >= context.end()) { + break; // Event is for a future cycle + } + + // Execute event + ev->execute(context); + ++n_processed; + + // Unblock pre-processing if this is a non-bundled atomic event + if (ev->get_execution() == Event::Execution::ATOMIC) { + assert(_block_state.load() == BlockState::PROCESSING); + _block_state = BlockState::UNBLOCKED; + } + + // Move to next event + last = ev; + ev = ev->next(); + + if (_block_state != BlockState::PROCESSING && + limit && n_processed >= limit) { + break; + } + } + + if (n_processed > 0) { +#ifndef NDEBUG + Engine& engine = context.engine(); + if (engine.world()->conf().option("trace").get<int32_t>()) { + const uint64_t start = engine.cycle_start_time(context); + const uint64_t end = engine.current_time(); + fprintf(stderr, "Processed %zu events in %u us\n", + n_processed, (unsigned)(end - start)); + } +#endif + + Event* next = (Event*)last->next(); + last->next(nullptr); + dest.append(context, head, last); + + // Since _head was not NULL, we know it hasn't been changed since + _head = next; + + /* If next is NULL, then _tail may now be invalid. However, it would cause + a race to reset _tail here. Instead, append() checks only _head for + emptiness, and resets the tail appropriately. */ + } + + return n_processed; +} + +void +PreProcessor::run() +{ + PreProcessContext ctx; + + UndoStack& undo_stack = *_engine.undo_stack(); + UndoStack& redo_stack = *_engine.redo_stack(); + AtomWriter undo_writer( + _engine.world()->uri_map(), _engine.world()->uris(), undo_stack); + AtomWriter redo_writer( + _engine.world()->uri_map(), _engine.world()->uris(), redo_stack); + + ThreadManager::set_flag(THREAD_PRE_PROCESS); + + Event* back = nullptr; + while (!_exit_flag) { + if (!_sem.timed_wait(std::chrono::seconds(1))) { + continue; + } + + if (!back) { + // Ran off end, find new unprepared back + back = _head; + while (back && back->is_prepared()) { + back = back->next(); + } + } + + Event* const ev = back; + if (!ev) { + continue; + } + + // Set block state before enqueueing event + switch (ev->get_execution()) { + case Event::Execution::NORMAL: + break; + case Event::Execution::ATOMIC: + assert(_block_state == BlockState::UNBLOCKED); + _block_state = BlockState::PRE_BLOCKED; + break; + case Event::Execution::BLOCK: + assert(_block_state == BlockState::UNBLOCKED); + _block_state = BlockState::PRE_BLOCKED; + break; + case Event::Execution::UNBLOCK: + wait_for_block_state(BlockState::BLOCKED); + _block_state = BlockState::PRE_UNBLOCKED; + } + + // Prepare event, allowing it to be processed + assert(!ev->is_prepared()); + if (ev->pre_process(ctx)) { + switch (ev->get_mode()) { + case Event::Mode::NORMAL: + case Event::Mode::REDO: + undo_stack.start_entry(); + ev->undo(undo_writer); + undo_stack.finish_entry(); + // undo_stack.save(stderr); + break; + case Event::Mode::UNDO: + redo_stack.start_entry(); + ev->undo(redo_writer); + redo_stack.finish_entry(); + // redo_stack.save(stderr, "redo"); + break; + } + } + assert(ev->is_prepared()); + + // Wait for process() if necessary + if (ev->get_execution() == Event::Execution::ATOMIC) { + wait_for_block_state(BlockState::UNBLOCKED); + } + + back = (Event*)ev->next(); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/PreProcessor.hpp b/src/server/PreProcessor.hpp new file mode 100644 index 00000000..eb72328e --- /dev/null +++ b/src/server/PreProcessor.hpp @@ -0,0 +1,87 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PREPROCESSOR_HPP +#define INGEN_ENGINE_PREPROCESSOR_HPP + +#include <atomic> +#include <thread> +#include <mutex> + +#include "raul/Semaphore.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class Event; +class PostProcessor; +class RunContext; + +class PreProcessor +{ +public: + explicit PreProcessor(Engine& engine); + + ~PreProcessor(); + + /** Return true iff no events are enqueued. */ + inline bool empty() const { return !_head.load(); } + + /** Enqueue an event. + * This is safe to call from any non-realtime thread (it locks). + */ + void event(Event* ev, Event::Mode mode); + + /** Process events for a cycle. + * @return The number of events processed. + */ + unsigned process(RunContext& context, + PostProcessor& dest, + size_t limit = 0); + +protected: + void run(); + +private: + enum class BlockState { + UNBLOCKED, ///< Normal, unblocked execution + PRE_BLOCKED, ///< Preprocess thread has enqueued blocking event + BLOCKED, ///< Process thread has reached blocking event + PRE_UNBLOCKED, ///< Preprocess thread has enqueued unblocking event + PROCESSING ///< Process thread is executing all events in-between + }; + + void wait_for_block_state(const BlockState state) { + while (_block_state != state) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + + Engine& _engine; + std::mutex _mutex; + Raul::Semaphore _sem; + std::atomic<Event*> _head; + std::atomic<Event*> _tail; + std::atomic<BlockState> _block_state; + bool _exit_flag; + std::thread _thread; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PREPROCESSOR_HPP diff --git a/src/server/RunContext.cpp b/src/server/RunContext.cpp new file mode 100644 index 00000000..3ab9d15c --- /dev/null +++ b/src/server/RunContext.cpp @@ -0,0 +1,195 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" + +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "Task.hpp" + +namespace Ingen { +namespace Server { + +struct Notification +{ + inline Notification(PortImpl* p = nullptr, + FrameTime f = 0, + LV2_URID k = 0, + uint32_t s = 0, + LV2_URID t = 0) + : port(p), time(f), key(k), size(s), type(t) + {} + + PortImpl* port; + FrameTime time; + LV2_URID key; + uint32_t size; + LV2_URID type; +}; + +RunContext::RunContext(Engine& engine, + Raul::RingBuffer* event_sink, + unsigned id, + bool threaded) + : _engine(engine) + , _event_sink(event_sink) + , _task(nullptr) + , _thread(threaded ? new std::thread(&RunContext::run, this) : nullptr) + , _id(id) + , _start(0) + , _end(0) + , _offset(0) + , _nframes(0) + , _realtime(true) +{} + +RunContext::RunContext(const RunContext& copy) + : _engine(copy._engine) + , _event_sink(copy._event_sink) + , _task(nullptr) + , _thread(nullptr) + , _id(copy._id) + , _start(copy._start) + , _end(copy._end) + , _offset(copy._offset) + , _nframes(copy._nframes) + , _realtime(copy._realtime) +{} + +bool +RunContext::must_notify(const PortImpl* port) const +{ + return (port->is_monitored() || _engine.broadcaster()->must_broadcast()); +} + +bool +RunContext::notify(LV2_URID key, + FrameTime time, + PortImpl* port, + uint32_t size, + LV2_URID type, + const void* body) +{ + const Notification n(port, time, key, size, type); + if (_event_sink->write_space() < sizeof(n) + size) { + return false; + } + if (_event_sink->write(sizeof(n), &n) != sizeof(n)) { + _engine.log().rt_error("Error writing header to notification ring\n"); + } else if (_event_sink->write(size, body) != size) { + _engine.log().rt_error("Error writing body to notification ring\n"); + } else { + return true; + } + return false; +} + +void +RunContext::emit_notifications(FrameTime end) +{ + const URIs& uris = _engine.buffer_factory()->uris(); + const uint32_t read_space = _event_sink->read_space(); + Notification note; + for (uint32_t i = 0; i < read_space; i += sizeof(note)) { + if (_event_sink->peek(sizeof(note), ¬e) != sizeof(note) || + note.time >= end) { + return; + } + if (_event_sink->read(sizeof(note), ¬e) == sizeof(note)) { + Atom value = _engine.world()->forge().alloc( + note.size, note.type, nullptr); + if (_event_sink->read(note.size, value.get_body()) == note.size) { + i += note.size; + const char* key = _engine.world()->uri_map().unmap_uri(note.key); + if (key) { + _engine.broadcaster()->set_property( + note.port->uri(), URI(key), value); + if (note.port->is_input() && + (note.key == uris.ingen_value || + note.key == uris.midi_binding)) { + // FIXME: not thread safe + note.port->set_property(URI(key), value); + } + } else { + _engine.log().rt_error("Error unmapping notification key URI\n"); + } + } else { + _engine.log().rt_error("Error reading body from notification ring\n"); + } + } else { + _engine.log().rt_error("Error reading header from notification ring\n"); + } + } +} + +void +RunContext::claim_task(Task* task) +{ + if ((_task = task)) { + _engine.signal_tasks_available(); + } +} + +Task* +RunContext::steal_task() const +{ + return _engine.steal_task(_id + 1); +} + +void +RunContext::set_priority(int priority) +{ + if (_thread) { + pthread_t pthread = _thread->native_handle(); + const int policy = (priority > 0) ? SCHED_FIFO : SCHED_OTHER; + sched_param sp; + sp.sched_priority = (priority > 0) ? priority : 0; + if (pthread_setschedparam(pthread, policy, &sp)) { + _engine.log().error( + fmt("Failed to set real-time priority of run thread (%s)\n") + % strerror(errno)); + } + } +} + +void +RunContext::join() +{ + if (_thread) { + if (_thread->joinable()) { + _thread->join(); + } + delete _thread; + } +} + +void +RunContext::run() +{ + while (_engine.wait_for_tasks()) { + for (Task* t; (t = _engine.steal_task(0));) { + t->run(*this); + } + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/RunContext.hpp b/src/server/RunContext.hpp new file mode 100644 index 00000000..bb64a250 --- /dev/null +++ b/src/server/RunContext.hpp @@ -0,0 +1,161 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_RUNCONTEXT_HPP +#define INGEN_ENGINE_RUNCONTEXT_HPP + +#include <cstdint> +#include <thread> + +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "raul/RingBuffer.hpp" + +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class PortImpl; +class Task; + +/** Graph execution context. + * + * This is used to pass whatever information a Node might need to process; such + * as the current time, a sink for generated events, etc. + * + * Note the logical distinction between nframes (jack relative) and start/end + * (timeline relative). If transport speed != 1.0, then end-start != nframes + * (though currently this is never the case, it may be if ingen incorporates + * tempo and varispeed). + * + * \ingroup engine + */ +class RunContext +{ +public: + /** Create a new run context. + * + * @param engine The engine this context is running within. + * @param event_sink Sink for notification events (peaks etc) + * @param id The ID of this context. + * @param threaded If true, then this context is a worker which will launch + * a thread and execute tasks as they become available. + */ + RunContext(Engine& engine, + Raul::RingBuffer* event_sink, + unsigned id, + bool threaded); + + /** Create a sub-context of `parent`. + * + * This is used to subdivide process cycles, the sub-context is + * lightweight and only serves to pass different time attributes. + */ + RunContext(const RunContext& copy); + + /** Return true iff the given port should broadcast its value. + * + * Whether or not broadcasting is actually done is a per-client property, + * this is for use in the audio thread to quickly determine if the + * necessary calculations need to be done at all. + */ + bool must_notify(const PortImpl* port) const; + + /** Send a notification from this run context. + * @return false on failure (ring is full) + */ + bool notify(LV2_URID key = 0, + FrameTime time = 0, + PortImpl* port = nullptr, + uint32_t size = 0, + LV2_URID type = 0, + const void* body = nullptr); + + /** Emit pending notifications in some other non-realtime thread. */ + void emit_notifications(FrameTime end); + + /** Return true iff any notifications are pending. */ + bool pending_notifications() const { return _event_sink->read_space(); } + + /** Return the duration of this cycle in microseconds. + * + * This is the cycle length in frames (nframes) converted to microseconds, + * that is, the amount of real time that this cycle's audio represents. + * Note that this is unrelated to the amount of time available to execute a + * cycle (other than the fact that it must be processed in significantly + * less time to avoid a dropout when running in real time). + */ + inline uint64_t duration() const { + return (uint64_t)_nframes * 1e6 / _rate; + } + + inline void locate(FrameTime s, SampleCount nframes) { + _start = s; + _end = s + nframes; + _nframes = nframes; + } + + inline void slice(SampleCount offset, SampleCount nframes) { + _offset = offset; + _nframes = nframes; + } + + /** Claim a parallel task, and signal others that work is available. */ + void claim_task(Task* task); + + /** Steal a task from some other context if possible. */ + Task* steal_task() const; + + void set_priority(int priority); + void set_rate(SampleCount rate) { _rate = rate; } + + void join(); + + inline Engine& engine() const { return _engine; } + inline Task* task() const { return _task; } + inline unsigned id() const { return _id; } + inline FrameTime start() const { return _start; } + inline FrameTime time() const { return _start + _offset; } + inline FrameTime end() const { return _end; } + inline SampleCount offset() const { return _offset; } + inline SampleCount nframes() const { return _nframes; } + inline SampleCount rate() const { return _rate; } + inline bool realtime() const { return _realtime; } + +protected: + const RunContext& operator=(const RunContext& copy) = delete; + + void run(); + + Engine& _engine; ///< Engine we're running in + Raul::RingBuffer* _event_sink; ///< Port updates from process context + Task* _task; ///< Currently executing task + std::thread* _thread; ///< Thread (NULL for main run context) + unsigned _id; ///< Context ID + + FrameTime _start; ///< Start frame of this cycle, timeline relative + FrameTime _end; ///< End frame of this cycle, timeline relative + SampleCount _offset; ///< Offset into data buffers + SampleCount _nframes; ///< Number of frames past offset to process + SampleCount _rate; ///< Sample rate in Hz + bool _realtime; ///< True iff context is hard realtime +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_RUNCONTEXT_HPP diff --git a/src/server/SocketListener.cpp b/src/server/SocketListener.cpp new file mode 100644 index 00000000..a6faa863 --- /dev/null +++ b/src/server/SocketListener.cpp @@ -0,0 +1,190 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <poll.h> +#include <signal.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <cerrno> +#include <sstream> +#include <string> +#include <thread> + +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/World.hpp" +#include "raul/Socket.hpp" + +#include "../server/Engine.hpp" +#include "../server/EventWriter.hpp" + +#include "SocketListener.hpp" +#include "SocketServer.hpp" + +namespace Ingen { +namespace Server { + +static constexpr const char* const unix_scheme = "unix://"; + +static std::string +get_link_target(const char* link_path) +{ + // Stat the link to get the required size for the target path + struct stat link_stat; + if (lstat(link_path, &link_stat)) { + return std::string(); + } + + // Allocate buffer and read link target + char* target = (char*)calloc(1, link_stat.st_size + 1); + if (readlink(link_path, target, link_stat.st_size) != -1) { + const std::string result(target); + free(target); + return result; + } + + return std::string(); +} + +static void ingen_listen(Engine* engine, + Raul::Socket* unix_sock, + Raul::Socket* net_sock); + + +SocketListener::SocketListener(Engine& engine) + : unix_sock(Raul::Socket::Type::UNIX) + , net_sock(Raul::Socket::Type::TCP) + , thread(new std::thread(ingen_listen, &engine, &unix_sock, &net_sock)) +{} + +SocketListener::~SocketListener() { + unix_sock.shutdown(); + net_sock.shutdown(); + thread->join(); + unlink(unix_sock.uri().substr(strlen(unix_scheme)).c_str()); +} + +static void +ingen_listen(Engine* engine, Raul::Socket* unix_sock, Raul::Socket* net_sock) +{ + Ingen::World* world = engine->world(); + + const std::string link_path(world->conf().option("socket").ptr<char>()); + const std::string unix_path(link_path + "." + std::to_string(getpid())); + + // Bind UNIX socket and create PID-less symbolic link + const URI unix_uri(unix_scheme + unix_path); + bool make_link = true; + if (!unix_sock->bind(unix_uri) || !unix_sock->listen()) { + world->log().error("Failed to create UNIX socket\n"); + unix_sock->close(); + make_link = false; + } else { + const std::string old_path = get_link_target(link_path.c_str()); + if (!old_path.empty()) { + const std::string suffix = old_path.substr(old_path.find_last_of(".") + 1); + const pid_t pid = std::stoi(suffix); + if (!kill(pid, 0)) { + make_link = false; + world->log().warn(fmt("Another Ingen instance is running at %1% => %2%\n") + % link_path % old_path); + } else { + world->log().warn(fmt("Replacing old link %1% => %2%\n") + % link_path % old_path); + unlink(link_path.c_str()); + } + } + + if (make_link) { + if (!symlink(unix_path.c_str(), link_path.c_str())) { + world->log().info(fmt("Listening on %1%\n") % + (unix_scheme + link_path)); + } else { + world->log().error(fmt("Failed to link %1% => %2% (%3%)\n") + % link_path % unix_path % strerror(errno)); + } + } else { + world->log().info(fmt("Listening on %1%\n") % unix_uri); + } + } + + // Bind TCP socket + const int port = world->conf().option("engine-port").get<int32_t>(); + std::ostringstream ss; + ss << "tcp://*:" << port; + if (!net_sock->bind(URI(ss.str())) || !net_sock->listen()) { + world->log().error("Failed to create TCP socket\n"); + net_sock->close(); + } else { + world->log().info(fmt("Listening on TCP port %1%\n") % port); + } + + if (unix_sock->fd() == -1 && net_sock->fd() == -1) { + return; // No sockets to listen to, exit thread + } + + struct pollfd pfds[2]; + int nfds = 0; + if (unix_sock->fd() != -1) { + pfds[nfds].fd = unix_sock->fd(); + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + ++nfds; + } + if (net_sock->fd() != -1) { + pfds[nfds].fd = net_sock->fd(); + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + ++nfds; + } + + while (true) { + // Wait for input to arrive at a socket + const int ret = poll(pfds, nfds, -1); + if (ret == -1) { + world->log().error(fmt("Poll error: %1%\n") % strerror(errno)); + break; + } else if (ret == 0) { + world->log().warn("Poll returned with no data\n"); + continue; + } else if ((pfds[0].revents & POLLHUP) || pfds[1].revents & POLLHUP) { + break; + } + + if (pfds[0].revents & POLLIN) { + SPtr<Raul::Socket> conn = unix_sock->accept(); + if (conn) { + new SocketServer(*world, *engine, conn); + } + } + + if (pfds[1].revents & POLLIN) { + SPtr<Raul::Socket> conn = net_sock->accept(); + if (conn) { + new SocketServer(*world, *engine, conn); + } + } + } + + if (make_link) { + unlink(link_path.c_str()); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/SocketListener.hpp b/src/server/SocketListener.hpp new file mode 100644 index 00000000..e74629ad --- /dev/null +++ b/src/server/SocketListener.hpp @@ -0,0 +1,41 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <memory> +#include <thread> + +#include "raul/Socket.hpp" + +namespace Ingen { +namespace Server { + +class Engine; + +/** Listens on main sockets and spawns socket servers for new connections. */ +class SocketListener +{ +public: + SocketListener(Engine& engine); + ~SocketListener(); + +private: + Raul::Socket unix_sock; + Raul::Socket net_sock; + std::unique_ptr<std::thread> thread; +}; + +} // namespace Server +} // namespace Ingen diff --git a/src/server/SocketServer.hpp b/src/server/SocketServer.hpp new file mode 100644 index 00000000..dbeb76ea --- /dev/null +++ b/src/server/SocketServer.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_SERVER_SOCKET_SERVER_HPP +#define INGEN_SERVER_SOCKET_SERVER_HPP + +#include "ingen/SocketReader.hpp" +#include "ingen/SocketWriter.hpp" +#include "ingen/StreamWriter.hpp" +#include "ingen/Tee.hpp" +#include "raul/Socket.hpp" + +#include "EventWriter.hpp" + +namespace Ingen { +namespace Server { + +/** The server side of an Ingen socket connection. */ +class SocketServer +{ +public: + SocketServer(World& world, + Server::Engine& engine, + SPtr<Raul::Socket> sock) + : _engine(engine) + , _sink(world.conf().option("dump").get<int32_t>() + ? SPtr<Interface>( + new Tee({SPtr<Interface>(new EventWriter(engine)), + SPtr<Interface>(new StreamWriter(world.uri_map(), + world.uris(), + URI("ingen:/engine"), + stderr, + ColorContext::Color::CYAN))})) + : SPtr<Interface>(new EventWriter(engine))) + , _reader(new SocketReader(world, *_sink.get(), sock)) + , _writer(new SocketWriter(world.uri_map(), + world.uris(), + URI(sock->uri()), + sock)) + { + _sink->set_respondee(_writer); + engine.register_client(_writer); + } + + ~SocketServer() { + if (_writer) { + _engine.unregister_client(_writer); + } + } + +protected: + void on_hangup() { + _engine.unregister_client(_writer); + _writer.reset(); + } + +private: + Server::Engine& _engine; + SPtr<Interface> _sink; + SPtr<SocketReader> _reader; + SPtr<SocketWriter> _writer; +}; + +} // namespace Ingen +} // namespace Socket + +#endif // INGEN_SERVER_SOCKET_SERVER_HPP diff --git a/src/server/Task.cpp b/src/server/Task.cpp new file mode 100644 index 00000000..d2cb2683 --- /dev/null +++ b/src/server/Task.cpp @@ -0,0 +1,158 @@ +/* + This file is part of Ingen. + Copyright 2015-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "BlockImpl.hpp" +#include "Task.hpp" + +namespace Ingen { +namespace Server { + +void +Task::run(RunContext& context) +{ + switch (_mode) { + case Mode::SINGLE: + // fprintf(stderr, "%u run %s\n", context.id(), _block->path().c_str()); + _block->process(context); + break; + case Mode::SEQUENTIAL: + for (const auto& task : _children) { + task->run(context); + } + break; + case Mode::PARALLEL: + // Initialize (not) done state of sub-tasks + for (const auto& task : _children) { + task->set_done(false); + } + + // Grab the first sub-task + _next = 0; + _done_end = 0; + Task* t = steal(context); + + // Allow other threads to steal sub-tasks + context.claim_task(this); + + // Run available tasks until this task is finished + for (; t; t = get_task(context)) { + t->run(context); + } + context.claim_task(nullptr); + break; + } + + set_done(true); +} + +Task* +Task::steal(RunContext& context) +{ + if (_mode == Mode::PARALLEL) { + const unsigned i = _next++; + if (i < _children.size()) { + return _children[i].get(); + } + } + + return nullptr; +} + +Task* +Task::get_task(RunContext& context) +{ + // Attempt to "steal" a task from ourselves + Task* t = steal(context); + if (t) { + return t; + } + + while (true) { + // Push done end index as forward as possible + while (_done_end < _children.size() && _children[_done_end]->done()) { + ++_done_end; + } + + if (_done_end >= _children.size()) { + return nullptr; // All child tasks are finished + } + + // All child tasks claimed, but some are unfinished, steal a task + if ((t = context.steal_task())) { + return t; + } + + /* All child tasks are claimed, and we failed to steal any tasks. Spin + to prevent blocking, though it would probably be wiser to wait for a + signal in non-main threads, and maybe even in the main thread + depending on your real-time safe philosophy... more experimentation + here is needed. */ + } +} + +std::unique_ptr<Task> +Task::simplify(std::unique_ptr<Task>&& task) +{ + if (task->mode() == Mode::SINGLE) { + return std::move(task); + } + + std::unique_ptr<Task> ret = std::unique_ptr<Task>(new Task(task->mode())); + for (auto&& c : task->_children) { + auto child = simplify(std::move(c)); + if (!child->empty()) { + if (child->mode() == task->mode()) { + // Merge child into parent + for (auto&& grandchild : child->_children) { + ret->append(std::move(grandchild)); + } + } else { + // Add child task + ret->append(std::move(child)); + } + } + } + + if (ret->_children.size() == 1) { + return std::move(ret->_children.front()); + } + + return ret; +} + +void +Task::dump(std::function<void (const std::string&)> sink, unsigned indent, bool first) const +{ + if (!first) { + sink("\n"); + for (unsigned i = 0; i < indent; ++i) { + sink(" "); + } + } + + if (_mode == Mode::SINGLE) { + sink(_block->path()); + } else { + sink(((_mode == Mode::SEQUENTIAL) ? "(seq " : "(par ")); + for (size_t i = 0; i < _children.size(); ++i) { + _children[i]->dump(sink, indent + 5, i == 0); + } + sink(")"); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Task.hpp b/src/server/Task.hpp new file mode 100644 index 00000000..2cdad71b --- /dev/null +++ b/src/server/Task.hpp @@ -0,0 +1,120 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_TASK_HPP +#define INGEN_ENGINE_TASK_HPP + +#include <atomic> +#include <cassert> +#include <deque> +#include <functional> +#include <memory> +#include <ostream> + +namespace Ingen { +namespace Server { + +class BlockImpl; +class RunContext; + +class Task { +public: + enum class Mode { + SINGLE, ///< Single block to run + SEQUENTIAL, ///< Elements must be run sequentially in order + PARALLEL ///< Elements may be run in any order in parallel + }; + + Task(Mode mode, BlockImpl* block = nullptr) + : _block(block) + , _mode(mode) + , _done_end(0) + , _next(0) + , _done(false) + { + assert(!(mode == Mode::SINGLE && !block)); + } + + Task(Task&& task) + : _children(std::move(task._children)) + , _block(task._block) + , _mode(task._mode) + , _done_end(task._done_end) + , _next(task._next.load()) + , _done(task._done.load()) + {} + + Task& operator=(Task&& task) + { + _children = std::move(task._children); + _block = task._block; + _mode = task._mode; + _done_end = task._done_end; + _next = task._next.load(); + _done = task._done.load(); + return *this; + } + + /** Run task in the given context. */ + void run(RunContext& context); + + /** Pretty print task to the given stream (recursively). */ + void dump(std::function<void (const std::string&)> sink, unsigned indent, bool first) const; + + /** Return true iff this is an empty task. */ + bool empty() const { return _mode != Mode::SINGLE && _children.empty(); } + + /** Simplify task expression. */ + static std::unique_ptr<Task> simplify(std::unique_ptr<Task>&& task); + + /** Steal a child task from this task (succeeds for PARALLEL only). */ + Task* steal(RunContext& context); + + /** Prepend a child to this task. */ + void push_front(Task&& task) { + _children.emplace_front(std::unique_ptr<Task>(new Task(std::move(task)))); + } + + Mode mode() const { return _mode; } + BlockImpl* block() const { return _block; } + bool done() const { return _done; } + + void set_done(bool done) { _done = done; } + +private: + typedef std::deque<std::unique_ptr<Task>> Children; + + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + + Task* get_task(RunContext& context); + + void append(std::unique_ptr<Task>&& t) { + _children.emplace_back(std::move(t)); + } + + Children _children; ///< Vector of child tasks + BlockImpl* _block; ///< Used for SINGLE only + Mode _mode; ///< Execution mode + unsigned _done_end; ///< Index of rightmost done sub-task + std::atomic<unsigned> _next; ///< Index of next sub-task + std::atomic<bool> _done; ///< Completion phase +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_TASK_HPP diff --git a/src/server/ThreadManager.hpp b/src/server/ThreadManager.hpp new file mode 100644 index 00000000..3bcedf30 --- /dev/null +++ b/src/server/ThreadManager.hpp @@ -0,0 +1,68 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_THREADMANAGER_HPP +#define INGEN_ENGINE_THREADMANAGER_HPP + +#include <cassert> + +#include "ingen/ingen.h" + +#include "util.hpp" + +namespace Ingen { +namespace Server { + +enum ThreadFlag { + THREAD_IS_REAL_TIME = 1, + THREAD_PRE_PROCESS = 1 << 1, + THREAD_PROCESS = 1 << 2, + THREAD_MESSAGE = 1 << 3, +}; + +class INGEN_API ThreadManager { +public: + static inline void set_flag(ThreadFlag f) { +#ifndef NDEBUG + flags = ((unsigned)flags | f); +#endif + } + + static inline void unset_flag(ThreadFlag f) { +#ifndef NDEBUG + flags = ((unsigned)flags & (~f)); +#endif + } + + static inline void assert_thread(ThreadFlag f) { + assert(single_threaded || (flags & f)); + } + + static inline void assert_not_thread(ThreadFlag f) { + assert(single_threaded || !(flags & f)); + } + + /** Set to true during initialisation so ensure_thread doesn't fail. + * Defined in Engine.cpp + */ + static bool single_threaded; + static INGEN_THREAD_LOCAL unsigned flags; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_THREADMANAGER_HPP diff --git a/src/server/UndoStack.cpp b/src/server/UndoStack.cpp new file mode 100644 index 00000000..dad211ad --- /dev/null +++ b/src/server/UndoStack.cpp @@ -0,0 +1,253 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <ctime> + +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "serd/serd.h" +#include "sratom/sratom.h" + +#include "UndoStack.hpp" + +#define NS_RDF (const uint8_t*)"http://www.w3.org/1999/02/22-rdf-syntax-ns#" + +#define USTR(s) ((const uint8_t*)(s)) + +namespace Ingen { +namespace Server { + +int +UndoStack::start_entry() +{ + if (_depth == 0) { + time_t now; + time(&now); + _stack.push_back(Entry(now)); + } + return ++_depth; +} + +bool +UndoStack::write(const LV2_Atom* msg, int32_t default_id) +{ + _stack.back().push_event(msg); + return true; +} + +bool +UndoStack::ignore_later_event(const LV2_Atom* first, + const LV2_Atom* second) const +{ + if (first->type != _uris.atom_Object || first->type != second->type) { + return false; + } + + const LV2_Atom_Object* f = (const LV2_Atom_Object*)first; + const LV2_Atom_Object* s = (const LV2_Atom_Object*)second; + if (f->body.otype == _uris.patch_Set && f->body.otype == s->body.otype) { + const LV2_Atom* f_subject = nullptr; + const LV2_Atom* f_property = nullptr; + const LV2_Atom* s_subject = nullptr; + const LV2_Atom* s_property = nullptr; + lv2_atom_object_get(f, + (LV2_URID)_uris.patch_subject, &f_subject, + (LV2_URID)_uris.patch_property, &f_property, + 0); + lv2_atom_object_get(s, + (LV2_URID)_uris.patch_subject, &s_subject, + (LV2_URID)_uris.patch_property, &s_property, + 0); + return (lv2_atom_equals(f_subject, s_subject) && + lv2_atom_equals(f_property, s_property)); + } + + return false; +} + +int +UndoStack::finish_entry() +{ + if (--_depth > 0) { + return _depth; + } else if (_stack.back().events.empty()) { + // Disregard empty entry + _stack.pop_back(); + } else if (_stack.size() > 1 && _stack.back().events.size() == 1) { + // This entry and the previous one have one event, attempt to merge + auto i = _stack.rbegin(); + ++i; + if (i->events.size() == 1) { + if (ignore_later_event(i->events[0], _stack.back().events[0])) { + _stack.pop_back(); + } + } + } + + return _depth; +} + +UndoStack::Entry +UndoStack::pop() +{ + Entry top; + if (!_stack.empty()) { + top = _stack.back(); + _stack.pop_back(); + } + return top; +} + +struct BlankIDs { + BlankIDs(char c='b') : n(0), c(c) {} + + SerdNode get() { + snprintf(buf, sizeof(buf), "%c%u", c, n++); + return serd_node_from_string(SERD_BLANK, USTR(buf)); + } + + char buf[16]; + unsigned n; + const char c; +}; + +struct ListContext { + explicit ListContext(BlankIDs& ids, unsigned flags, const SerdNode* s, const SerdNode* p) + : ids(ids) + , s(*s) + , p(*p) + , flags(flags | SERD_LIST_O_BEGIN) + {} + + SerdNode start_node(SerdWriter* writer) { + const SerdNode node = ids.get(); + serd_writer_write_statement(writer, flags, nullptr, &s, &p, &node, nullptr, nullptr); + return node; + } + + void append(SerdWriter* writer, unsigned oflags, const SerdNode* value) { + // s p node + const SerdNode node = start_node(writer); + + // node rdf:first value + p = serd_node_from_string(SERD_URI, NS_RDF "first"); + flags = SERD_LIST_CONT; + serd_writer_write_statement(writer, flags|oflags, nullptr, &node, &p, value, nullptr, nullptr); + + end_node(writer, &node); + } + + void end_node(SerdWriter* writer, const SerdNode* node) { + // Prepare for next call: node rdf:rest ... + s = *node; + p = serd_node_from_string(SERD_URI, NS_RDF "rest"); + } + + void end(SerdWriter* writer) { + const SerdNode nil = serd_node_from_string(SERD_URI, NS_RDF "nil"); + serd_writer_write_statement(writer, flags, nullptr, &s, &p, &nil, nullptr, nullptr); + } + + BlankIDs& ids; + SerdNode s; + SerdNode p; + unsigned flags; +}; + +void +UndoStack::write_entry(Sratom* sratom, + SerdWriter* writer, + const SerdNode* const subject, + const UndoStack::Entry& entry) +{ + char time_str[24]; + strftime(time_str, sizeof(time_str), "%FT%T", gmtime(&entry.time)); + + // entry rdf:type ingen:UndoEntry + SerdNode p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "time")); + SerdNode o = serd_node_from_string(SERD_LITERAL, USTR(time_str)); + serd_writer_write_statement(writer, SERD_ANON_CONT, nullptr, subject, &p, &o, nullptr, nullptr); + + p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "events")); + + BlankIDs ids('e'); + ListContext ctx(ids, SERD_ANON_CONT, subject, &p); + + for (const LV2_Atom* atom : entry.events) { + const SerdNode node = ctx.start_node(writer); + + p = serd_node_from_string(SERD_URI, NS_RDF "first"); + ctx.flags = SERD_LIST_CONT; + sratom_write(sratom, &_map.urid_unmap_feature()->urid_unmap, SERD_LIST_CONT, + &node, &p, + atom->type, atom->size, LV2_ATOM_BODY_CONST(atom)); + + ctx.end_node(writer, &node); + } + + ctx.end(writer); +} + +void +UndoStack::save(FILE* stream, const char* name) +{ + SerdEnv* env = serd_env_new(nullptr); + serd_env_set_prefix_from_strings(env, USTR("atom"), USTR(LV2_ATOM_PREFIX)); + serd_env_set_prefix_from_strings(env, USTR("ingen"), USTR(INGEN_NS)); + serd_env_set_prefix_from_strings(env, USTR("patch"), USTR(LV2_PATCH_PREFIX)); + + const SerdNode base = serd_node_from_string(SERD_URI, USTR("ingen:/")); + SerdURI base_uri; + serd_uri_parse(base.buf, &base_uri); + + SerdWriter* writer = serd_writer_new( + SERD_TURTLE, + (SerdStyle)(SERD_STYLE_RESOLVED|SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED), + env, + &base_uri, + serd_file_sink, + stream); + + // Configure sratom to write directly to the writer (and thus the socket) + Sratom* sratom = sratom_new(&_map.urid_map_feature()->urid_map); + sratom_set_sink(sratom, + (const char*)base.buf, + (SerdStatementSink)serd_writer_write_statement, + (SerdEndSink)serd_writer_end_anon, + writer); + + SerdNode s = serd_node_from_string(SERD_BLANK, (const uint8_t*)name); + SerdNode p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "entries")); + + BlankIDs ids('u'); + ListContext ctx(ids, 0, &s, &p); + for (const Entry& e : _stack) { + const SerdNode entry = ids.get(); + ctx.append(writer, SERD_ANON_O_BEGIN, &entry); + write_entry(sratom, writer, &entry, e); + serd_writer_end_anon(writer, &entry); + } + ctx.end(writer); + + sratom_free(sratom); + serd_writer_finish(writer); + serd_writer_free(writer); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/UndoStack.hpp b/src/server/UndoStack.hpp new file mode 100644 index 00000000..6ce6475f --- /dev/null +++ b/src/server/UndoStack.hpp @@ -0,0 +1,107 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_UNDOSTACK_HPP +#define INGEN_ENGINE_UNDOSTACK_HPP + +#include <ctime> +#include <deque> + +#include "ingen/AtomSink.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "serd/serd.h" +#include "sratom/sratom.h" + +namespace Ingen { + +class URIMap; +class URIs; + +namespace Server { + +class INGEN_API UndoStack : public AtomSink { +public: + struct Entry { + Entry(time_t time=0) : time(time) {} + + Entry(const Entry& copy) + : time(copy.time) + { + for (const LV2_Atom* ev : copy.events) { + push_event(ev); + } + } + + ~Entry() { clear(); } + + Entry& operator=(const Entry& rhs) { + clear(); + time = rhs.time; + for (const LV2_Atom* ev : rhs.events) { + push_event(ev); + } + return *this; + } + + void clear() { + for (LV2_Atom* ev : events) { + free(ev); + } + events.clear(); + } + + void push_event(const LV2_Atom* ev) { + const uint32_t size = lv2_atom_total_size(ev); + LV2_Atom* copy = (LV2_Atom*)malloc(size); + memcpy(copy, ev, size); + events.push_front(copy); + } + + time_t time; + std::deque<LV2_Atom*> events; + }; + + UndoStack(URIs& uris, URIMap& map) : _uris(uris), _map(map), _depth(0) {} + + int start_entry(); + bool write(const LV2_Atom* msg, int32_t default_id=0); + int finish_entry(); + + bool empty() const { return _stack.empty(); } + Entry pop(); + + void save(FILE* stream, const char* name="undo"); + +private: + bool ignore_later_event(const LV2_Atom* first, + const LV2_Atom* second) const; + + void write_entry(Sratom* sratom, + SerdWriter* writer, + const SerdNode* subject, + const Entry& entry); + + URIs& _uris; + URIMap& _map; + std::deque<Entry> _stack; + int _depth; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_UNDOSTACK_HPP diff --git a/src/server/Worker.cpp b/src/server/Worker.cpp new file mode 100644 index 00000000..6f60250c --- /dev/null +++ b/src/server/Worker.cpp @@ -0,0 +1,163 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "lv2/lv2plug.in/ns/ext/worker/worker.h" + +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "LV2Block.hpp" +#include "Worker.hpp" + +namespace Ingen { +namespace Server { + +/// A message in the Worker::_requests ring +struct MessageHeader { + LV2Block* block; ///< Node this message is from + uint32_t size; ///< Size of following data + // `size' bytes of data follow here +}; + +static LV2_Worker_Status +schedule(LV2_Worker_Schedule_Handle handle, + uint32_t size, + const void* data) +{ + LV2Block* block = (LV2Block*)handle; + Engine& engine = block->parent_graph()->engine(); + + return engine.worker()->request(block, size, data); +} + +static LV2_Worker_Status +schedule_sync(LV2_Worker_Schedule_Handle handle, + uint32_t size, + const void* data) +{ + LV2Block* block = (LV2Block*)handle; + Engine& engine = block->parent_graph()->engine(); + + return engine.sync_worker()->request(block, size, data); +} + +LV2_Worker_Status +Worker::request(LV2Block* block, + uint32_t size, + const void* data) +{ + if (_synchronous) { + return block->work(size, data); + } + + Engine& engine = block->parent_graph()->engine(); + if (_requests.write_space() < sizeof(MessageHeader) + size) { + engine.log().error("Work request ring overflow\n"); + return LV2_WORKER_ERR_NO_SPACE; + } + + const MessageHeader msg = { block, size }; + if (_requests.write(sizeof(msg), &msg) != sizeof(msg)) { + engine.log().error("Error writing header to work request ring\n"); + return LV2_WORKER_ERR_UNKNOWN; + } + if (_requests.write(size, data) != size) { + engine.log().error("Error writing body to work request ring\n"); + return LV2_WORKER_ERR_UNKNOWN; + } + + _sem.post(); + + return LV2_WORKER_SUCCESS; +} + +SPtr<LV2_Feature> +Worker::Schedule::feature(World* world, Node* n) +{ + LV2Block* block = dynamic_cast<LV2Block*>(n); + if (!block) { + return SPtr<LV2_Feature>(); + } + + LV2_Worker_Schedule* data = (LV2_Worker_Schedule*)malloc( + sizeof(LV2_Worker_Schedule)); + data->handle = block; + data->schedule_work = synchronous ? schedule_sync : schedule; + + LV2_Feature* f = (LV2_Feature*)malloc(sizeof(LV2_Feature)); + f->URI = LV2_WORKER__schedule; + f->data = data; + + return SPtr<LV2_Feature>(f, &free_feature); +} + +Worker::Worker(Log& log, uint32_t buffer_size, bool synchronous) + : _schedule(new Schedule(synchronous)) + , _log(log) + , _sem(0) + , _requests(buffer_size) + , _responses(buffer_size) + , _buffer((uint8_t*)malloc(buffer_size)) + , _buffer_size(buffer_size) + , _thread(nullptr) + , _exit_flag(false) + , _synchronous(synchronous) +{ + if (!synchronous) { + _thread = new std::thread(&Worker::run, this); + } +} + +Worker::~Worker() +{ + _exit_flag = true; + _sem.post(); + if (_thread) { + _thread->join(); + delete _thread; + } + free(_buffer); +} + +void +Worker::run() +{ + while (_sem.wait() && !_exit_flag) { + MessageHeader msg; + if (_requests.read_space() > sizeof(msg)) { + if (_requests.read(sizeof(msg), &msg) != sizeof(msg)) { + _log.error("Error reading header from work request ring\n"); + continue; + } + + if (msg.size >= _buffer_size - sizeof(msg)) { + _log.error("Corrupt work request ring\n"); + return; + } + + if (_requests.read(msg.size, _buffer) != msg.size) { + _log.error("Error reading body from work request ring\n"); + continue; + } + + msg.block->work(msg.size, _buffer); + } + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Worker.hpp b/src/server/Worker.hpp new file mode 100644 index 00000000..0a3fdeaf --- /dev/null +++ b/src/server/Worker.hpp @@ -0,0 +1,76 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_WORKER_HPP +#define INGEN_ENGINE_WORKER_HPP + +#include <thread> + +#include "ingen/LV2Features.hpp" +#include "lv2/lv2plug.in/ns/ext/worker/worker.h" +#include "raul/RingBuffer.hpp" +#include "raul/Semaphore.hpp" + +namespace Ingen { + +class Log; + +namespace Server { + +class LV2Block; + +class Worker +{ +public: + Worker(Log& log, uint32_t buffer_size, bool synchronous=false); + ~Worker(); + + struct Schedule : public LV2Features::Feature { + Schedule(bool sync) : synchronous(sync) {} + + const char* uri() const { return LV2_WORKER__schedule; } + + SPtr<LV2_Feature> feature(World* world, Node* n); + + const bool synchronous; + }; + + LV2_Worker_Status request(LV2Block* block, + uint32_t size, + const void* data); + + SPtr<Schedule> schedule_feature() { return _schedule; } + +private: + SPtr<Schedule> _schedule; + + Log& _log; + Raul::Semaphore _sem; + Raul::RingBuffer _requests; + Raul::RingBuffer _responses; + uint8_t* const _buffer; + const uint32_t _buffer_size; + std::thread* _thread; + bool _exit_flag; + bool _synchronous; + + void run(); +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_WORKER_HPP diff --git a/src/server/events.hpp b/src/server/events.hpp new file mode 100644 index 00000000..5f77b431 --- /dev/null +++ b/src/server/events.hpp @@ -0,0 +1,35 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_EVENTS_HPP +#define INGEN_ENGINE_EVENTS_HPP + +#include "events/Connect.hpp" +#include "events/Copy.hpp" +#include "events/CreateBlock.hpp" +#include "events/CreateGraph.hpp" +#include "events/CreatePort.hpp" +#include "events/Delete.hpp" +#include "events/Delta.hpp" +#include "events/Disconnect.hpp" +#include "events/DisconnectAll.hpp" +#include "events/Get.hpp" +#include "events/Mark.hpp" +#include "events/Move.hpp" +#include "events/SetPortValue.hpp" +#include "events/Undo.hpp" + +#endif // INGEN_ENGINE_EVENTS_HPP diff --git a/src/server/events/Connect.cpp b/src/server/events/Connect.cpp new file mode 100644 index 00000000..8937b327 --- /dev/null +++ b/src/server/events/Connect.cpp @@ -0,0 +1,188 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Store.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "ArcImpl.hpp" +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "Connect.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" +#include "internals/BlockDelay.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Connect::Connect(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Connect& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _graph(nullptr) + , _head(nullptr) +{} + +bool +Connect::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + Node* tail = _engine.store()->get(_msg.tail); + if (!tail) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.tail); + } + + Node* head = _engine.store()->get(_msg.head); + if (!head) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.head); + } + + PortImpl* tail_output = dynamic_cast<PortImpl*>(tail); + _head = dynamic_cast<InputPort*>(head); + if (!tail_output || !_head) { + return Event::pre_process_done(Status::BAD_REQUEST, _msg.head); + } + + BlockImpl* const tail_block = tail_output->parent_block(); + BlockImpl* const head_block = _head->parent_block(); + if (!tail_block || !head_block) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, _msg.head); + } + + if (tail_block->parent() != head_block->parent() + && tail_block != head_block->parent() + && tail_block->parent() != head_block) { + return Event::pre_process_done(Status::PARENT_DIFFERS, _msg.head); + } + + if (!ArcImpl::can_connect(tail_output, _head)) { + return Event::pre_process_done(Status::TYPE_MISMATCH, _msg.head); + } + + if (tail_block->parent_graph() != head_block->parent_graph()) { + // Arc to a graph port from inside the graph + assert(tail_block->parent() == head_block || head_block->parent() == tail_block); + if (tail_block->parent() == head_block) { + _graph = dynamic_cast<GraphImpl*>(head_block); + } else { + _graph = dynamic_cast<GraphImpl*>(tail_block); + } + } else if (tail_block == head_block && dynamic_cast<GraphImpl*>(tail_block)) { + // Arc from a graph input to a graph output (pass through) + _graph = dynamic_cast<GraphImpl*>(tail_block); + } else { + // Normal arc between blocks with the same parent + _graph = tail_block->parent_graph(); + } + + if (_graph->has_arc(tail_output, _head)) { + return Event::pre_process_done(Status::EXISTS, _msg.head); + } + + _arc = SPtr<ArcImpl>(new ArcImpl(tail_output, _head)); + + /* Need to be careful about graph port arcs here and adding a + block's parent as a dependant/provider, or adding a graph as its own + provider... + */ + if (tail_block != head_block && tail_block->parent() == head_block->parent()) { + // Connection is between blocks inside a graph, compile graph + + // The tail block is now a dependency (provider) of the head block + head_block->providers().insert(tail_block); + + if (!dynamic_cast<Internals::BlockDelayNode*>(tail_block)) { + /* Arcs leaving a delay node are ignored for the purposes of + compilation, since the output is from the previous cycle and + does not affect execution order. Otherwise, the head block is + now a dependant of the head block. */ + tail_block->dependants().insert(head_block); + } + + if (ctx.must_compile(*_graph)) { + if (!(_compiled_graph = compile(*_engine.maid(), *_graph))) { + head_block->providers().erase(tail_block); + tail_block->dependants().erase(head_block); + return Event::pre_process_done(Status::COMPILATION_FAILED); + } + } + } + + _graph->add_arc(_arc); + _head->increment_num_arcs(); + + if (!_head->is_driver_port()) { + BufferFactory& bufs = *_engine.buffer_factory(); + _voices = bufs.maid().make_managed<PortImpl::Voices>(_head->poly()); + _head->pre_get_buffers(bufs, _voices, _head->poly()); + } + + tail_output->inherit_neighbour(_head, _tail_remove, _tail_add); + _head->inherit_neighbour(tail_output, _head_remove, _head_add); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Connect::execute(RunContext& context) +{ + if (_status == Status::SUCCESS) { + _head->add_arc(context, *_arc.get()); + if (!_head->is_driver_port()) { + _head->set_voices(context, std::move(_voices)); + } + _head->connect_buffers(); + if (_compiled_graph) { + _graph->set_compiled_graph(std::move(_compiled_graph)); + } + } +} + +void +Connect::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + if (!_tail_remove.empty() || !_tail_add.empty()) { + _engine.broadcaster()->delta( + path_to_uri(_msg.tail), _tail_remove, _tail_add); + } + if (!_tail_remove.empty() || !_tail_add.empty()) { + _engine.broadcaster()->delta( + path_to_uri(_msg.tail), _tail_remove, _tail_add); + } + } +} + +void +Connect::undo(Interface& target) +{ + target.disconnect(_msg.tail, _msg.head); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Connect.hpp b/src/server/events/Connect.hpp new file mode 100644 index 00000000..8a42b984 --- /dev/null +++ b/src/server/events/Connect.hpp @@ -0,0 +1,74 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_CONNECT_HPP +#define INGEN_EVENTS_CONNECT_HPP + +#include "raul/Path.hpp" + +#include "CompiledGraph.hpp" +#include "Event.hpp" +#include "PortImpl.hpp" +#include "types.hpp" + +namespace Raul { +template <typename T> class Array; +} + +namespace Ingen { +namespace Server { + +class ArcImpl; +class GraphImpl; +class InputPort; + +namespace Events { + +/** Make an Arc between two Ports. + * + * \ingroup engine + */ +class Connect : public Event +{ +public: + Connect(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Connect& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + const Ingen::Connect _msg; + GraphImpl* _graph; + InputPort* _head; + MPtr<CompiledGraph> _compiled_graph; + SPtr<ArcImpl> _arc; + MPtr<PortImpl::Voices> _voices; + Properties _tail_remove; + Properties _tail_add; + Properties _head_remove; + Properties _head_add; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_CONNECT_HPP diff --git a/src/server/events/Copy.cpp b/src/server/events/Copy.cpp new file mode 100644 index 00000000..fc9d40f7 --- /dev/null +++ b/src/server/events/Copy.cpp @@ -0,0 +1,216 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Parser.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/Store.hpp" +#include "raul/Path.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "Engine.hpp" +#include "EnginePort.hpp" +#include "GraphImpl.hpp" +#include "PreProcessContext.hpp" +#include "events/Copy.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Copy::Copy(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Copy& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _old_block(nullptr) + , _parent(nullptr) + , _block(nullptr) +{} + +bool +Copy::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + if (uri_is_path(_msg.old_uri)) { + // Old URI is a path within the engine + const Raul::Path old_path = uri_to_path(_msg.old_uri); + + // Find the old node + const Store::iterator i = _engine.store()->find(old_path); + if (i == _engine.store()->end()) { + return Event::pre_process_done(Status::NOT_FOUND, old_path); + } + + // Ensure it is a block (ports are not supported for now) + if (!(_old_block = dynamic_ptr_cast<BlockImpl>(i->second))) { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, old_path); + } + + if (uri_is_path(_msg.new_uri)) { + // Copy to path within the engine + return engine_to_engine(ctx); + } else if (_msg.new_uri.scheme() == "file") { + // Copy to filesystem path (i.e. save) + return engine_to_filesystem(ctx); + } else { + return Event::pre_process_done(Status::BAD_REQUEST); + } + } else if (_msg.old_uri.scheme() == "file") { + if (uri_is_path(_msg.new_uri)) { + return filesystem_to_engine(ctx); + } else { + // Ingen is not your file manager + return Event::pre_process_done(Status::BAD_REQUEST); + } + } + + return Event::pre_process_done(Status::BAD_URI); +} + +bool +Copy::engine_to_engine(PreProcessContext& ctx) +{ + // Only support a single source for now + const Raul::Path new_path = uri_to_path(_msg.new_uri); + if (!Raul::Symbol::is_valid(new_path.symbol())) { + return Event::pre_process_done(Status::BAD_REQUEST); + } + + // Ensure the new node doesn't already exist + if (_engine.store()->find(new_path) != _engine.store()->end()) { + return Event::pre_process_done(Status::EXISTS, new_path); + } + + // Find new parent graph + const Raul::Path parent_path = new_path.parent(); + const Store::iterator p = _engine.store()->find(parent_path); + if (p == _engine.store()->end()) { + return Event::pre_process_done(Status::NOT_FOUND, parent_path); + } + if (!(_parent = dynamic_cast<GraphImpl*>(p->second.get()))) { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, parent_path); + } + + // Create new block + if (!(_block = dynamic_cast<BlockImpl*>( + _old_block->duplicate(_engine, Raul::Symbol(new_path.symbol()), _parent)))) { + return Event::pre_process_done(Status::INTERNAL_ERROR); + } + + _block->activate(*_engine.buffer_factory()); + + // Add block to the store and the graph's pre-processor only block list + _parent->add_block(*_block); + _engine.store()->add(_block); + + // Compile graph with new block added for insertion in audio thread + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *_parent); + + return Event::pre_process_done(Status::SUCCESS); +} + +static bool +ends_with(const std::string& str, const std::string& end) +{ + if (str.length() >= end.length()) { + return !str.compare(str.length() - end.length(), end.length(), end); + } + return false; +} + +bool +Copy::engine_to_filesystem(PreProcessContext& ctx) +{ + // Ensure source is a graph + SPtr<GraphImpl> graph = dynamic_ptr_cast<GraphImpl>(_old_block); + if (!graph) { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, _msg.old_uri); + } + + if (!_engine.world()->serialiser()) { + return Event::pre_process_done(Status::INTERNAL_ERROR); + } + + std::lock_guard<std::mutex> lock(_engine.world()->rdf_mutex()); + + if (ends_with(_msg.new_uri, ".ingen") || ends_with(_msg.new_uri, ".ingen/")) { + _engine.world()->serialiser()->write_bundle(graph, URI(_msg.new_uri)); + } else { + _engine.world()->serialiser()->start_to_file(graph->path(), _msg.new_uri); + _engine.world()->serialiser()->serialise(graph); + _engine.world()->serialiser()->finish(); + } + + return Event::pre_process_done(Status::SUCCESS); +} + +bool +Copy::filesystem_to_engine(PreProcessContext& ctx) +{ + if (!_engine.world()->parser()) { + return Event::pre_process_done(Status::INTERNAL_ERROR); + } + + std::lock_guard<std::mutex> lock(_engine.world()->rdf_mutex()); + + // Old URI is a filesystem path and new URI is a path within the engine + const std::string src_path(_msg.old_uri.path()); + const Raul::Path dst_path = uri_to_path(_msg.new_uri); + boost::optional<Raul::Path> dst_parent; + boost::optional<Raul::Symbol> dst_symbol; + if (!dst_path.is_root()) { + dst_parent = dst_path.parent(); + dst_symbol = Raul::Symbol(dst_path.symbol()); + } + + _engine.world()->parser()->parse_file( + _engine.world(), _engine.world()->interface().get(), src_path, + dst_parent, dst_symbol); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Copy::execute(RunContext& context) +{ + if (_block && _compiled_graph) { + _parent->set_compiled_graph(std::move(_compiled_graph)); + } +} + +void +Copy::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + } +} + +void +Copy::undo(Interface& target) +{ + if (uri_is_path(_msg.new_uri)) { + target.del(_msg.new_uri); + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Copy.hpp b/src/server/events/Copy.hpp new file mode 100644 index 00000000..5216b56e --- /dev/null +++ b/src/server/events/Copy.hpp @@ -0,0 +1,68 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_COPY_HPP +#define INGEN_EVENTS_COPY_HPP + +#include <list> + +#include "ingen/Store.hpp" +#include "raul/Path.hpp" + +#include "CompiledGraph.hpp" +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; + +namespace Events { + +/** Copy a graph object to a new path. + * \ingroup engine + */ +class Copy : public Event +{ +public: + Copy(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Copy& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + bool engine_to_engine(PreProcessContext& ctx); + bool engine_to_filesystem(PreProcessContext& ctx); + bool filesystem_to_engine(PreProcessContext& ctx); + + const Ingen::Copy _msg; + SPtr<BlockImpl> _old_block; + GraphImpl* _parent; + BlockImpl* _block; + MPtr<CompiledGraph> _compiled_graph; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_COPY_HPP diff --git a/src/server/events/CreateBlock.cpp b/src/server/events/CreateBlock.cpp new file mode 100644 index 00000000..d678bea3 --- /dev/null +++ b/src/server/events/CreateBlock.cpp @@ -0,0 +1,180 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIs.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "BlockFactory.hpp" +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "CreateBlock.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" +#include "LV2Block.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +CreateBlock::CreateBlock(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + Properties& properties) + : Event(engine, client, id, timestamp) + , _path(path) + , _properties(properties) + , _graph(nullptr) + , _block(nullptr) +{} + +bool +CreateBlock::pre_process(PreProcessContext& ctx) +{ + typedef Properties::const_iterator iterator; + + const Ingen::URIs& uris = _engine.world()->uris(); + const SPtr<Store> store = _engine.store(); + + // Check sanity of target path + if (_path.is_root()) { + return Event::pre_process_done(Status::BAD_URI, _path); + } else if (store->get(_path)) { + return Event::pre_process_done(Status::EXISTS, _path); + } else if (!(_graph = dynamic_cast<GraphImpl*>(store->get(_path.parent())))) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, _path.parent()); + } + + // Map old ingen:prototype to new lv2:prototype + auto range = _properties.equal_range(uris.ingen_prototype); + for (auto i = range.first; i != range.second;) { + const auto value = i->second; + auto next = i; + next = _properties.erase(i); + _properties.emplace(uris.lv2_prototype, value); + i = next; + } + + // Get prototype + iterator t = _properties.find(uris.lv2_prototype); + if (t == _properties.end() || !uris.forge.is_uri(t->second)) { + // Missing/invalid prototype + return Event::pre_process_done(Status::BAD_REQUEST); + } + + const URI prototype(uris.forge.str(t->second, false)); + + // Find polyphony + const iterator p = _properties.find(uris.ingen_polyphonic); + const bool polyphonic = (p != _properties.end() && + p->second.type() == uris.forge.Bool && + p->second.get<int32_t>()); + + // Find and instantiate/duplicate prototype (plugin/existing node) + if (uri_is_path(prototype)) { + // Prototype is an existing block + BlockImpl* const ancestor = dynamic_cast<BlockImpl*>( + store->get(uri_to_path(prototype))); + if (!ancestor) { + return Event::pre_process_done(Status::PROTOTYPE_NOT_FOUND, prototype); + } else if (!(_block = ancestor->duplicate( + _engine, Raul::Symbol(_path.symbol()), _graph))) { + return Event::pre_process_done(Status::CREATION_FAILED, _path); + } + + /* Replace prototype with the ancestor's. This is less informative, + but the client expects an actual LV2 plugin as prototype. */ + _properties.erase(uris.ingen_prototype); + _properties.erase(uris.lv2_prototype); + _properties.emplace(uris.lv2_prototype, + uris.forge.make_urid(ancestor->plugin()->uri())); + } else { + // Prototype is a plugin + PluginImpl* const plugin = _engine.block_factory()->plugin(prototype); + if (!plugin) { + return Event::pre_process_done(Status::PROTOTYPE_NOT_FOUND, prototype); + } + + // Load state from directory if given in properties + LilvState* state = nullptr; + auto s = _properties.find(uris.state_state); + if (s != _properties.end() && s->second.type() == uris.forge.Path) { + state = LV2Block::load_state( + _engine.world(), FilePath(s->second.ptr<char>())); + } + + // Instantiate plugin + if (!(_block = plugin->instantiate(*_engine.buffer_factory(), + Raul::Symbol(_path.symbol()), + polyphonic, + _graph, + _engine, + state))) { + return Event::pre_process_done(Status::CREATION_FAILED, _path); + } + } + + // Activate block + _block->properties().insert(_properties.begin(), _properties.end()); + _block->activate(*_engine.buffer_factory()); + + // Add block to the store and the graph's pre-processor only block list + _graph->add_block(*_block); + store->add(_block); + + /* Compile graph with new block added for insertion in audio thread + TODO: Since the block is not connected at this point, a full compilation + could be avoided and the block simply appended. */ + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *_graph); + + _update.put_block(_block); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +CreateBlock::execute(RunContext& context) +{ + if (_status == Status::SUCCESS && _compiled_graph) { + _graph->set_compiled_graph(std::move(_compiled_graph)); + } +} + +void +CreateBlock::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _update.send(*_engine.broadcaster()); + } +} + +void +CreateBlock::undo(Interface& target) +{ + target.del(_block->uri()); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/CreateBlock.hpp b/src/server/events/CreateBlock.hpp new file mode 100644 index 00000000..0a29e68c --- /dev/null +++ b/src/server/events/CreateBlock.hpp @@ -0,0 +1,66 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_CREATEBLOCK_HPP +#define INGEN_EVENTS_CREATEBLOCK_HPP + +#include "ingen/Resource.hpp" + +#include "ClientUpdate.hpp" +#include "CompiledGraph.hpp" +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; + +namespace Events { + +/** An event to load a Block and insert it into a Graph. + * + * \ingroup engine + */ +class CreateBlock : public Event +{ +public: + CreateBlock(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + Properties& properties); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + Raul::Path _path; + Properties& _properties; + ClientUpdate _update; + GraphImpl* _graph; + BlockImpl* _block; + MPtr<CompiledGraph> _compiled_graph; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_CREATEBLOCK_HPP diff --git a/src/server/events/CreateGraph.cpp b/src/server/events/CreateGraph.cpp new file mode 100644 index 00000000..390fdd9a --- /dev/null +++ b/src/server/events/CreateGraph.cpp @@ -0,0 +1,236 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIs.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "Broadcaster.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PreProcessContext.hpp" +#include "events/CreateGraph.hpp" +#include "events/CreatePort.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +CreateGraph::CreateGraph(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + const Properties& properties) + : Event(engine, client, id, timestamp) + , _path(path) + , _properties(properties) + , _graph(nullptr) + , _parent(nullptr) +{} + +CreateGraph::~CreateGraph() +{ + for (Event* ev : _child_events) { + delete ev; + } +} + +void +CreateGraph::build_child_events() +{ + const Ingen::URIs& uris = _engine.world()->uris(); + + // Properties common to both ports + Properties control_properties; + control_properties.put(uris.atom_bufferType, uris.atom_Sequence); + control_properties.put(uris.atom_supports, uris.patch_Message); + control_properties.put(uris.lv2_designation, uris.lv2_control); + control_properties.put(uris.lv2_portProperty, uris.lv2_connectionOptional); + control_properties.put(uris.rdf_type, uris.atom_AtomPort); + control_properties.put(uris.rsz_minimumSize, uris.forge.make(4096)); + + // Add control port (message receive) + Properties in_properties(control_properties); + in_properties.put(uris.lv2_index, uris.forge.make(0)); + in_properties.put(uris.lv2_name, uris.forge.alloc("Control")); + in_properties.put(uris.rdf_type, uris.lv2_InputPort); + in_properties.put(uris.ingen_canvasX, uris.forge.make(32.0f), + Resource::Graph::EXTERNAL); + in_properties.put(uris.ingen_canvasY, uris.forge.make(32.0f), + Resource::Graph::EXTERNAL); + + _child_events.push_back( + new Events::CreatePort( + _engine, _request_client, -1, _time, + _path.child(Raul::Symbol("control")), + in_properties)); + + // Add notify port (message respond) + Properties out_properties(control_properties); + out_properties.put(uris.lv2_index, uris.forge.make(1)); + out_properties.put(uris.lv2_name, uris.forge.alloc("Notify")); + out_properties.put(uris.rdf_type, uris.lv2_OutputPort); + out_properties.put(uris.ingen_canvasX, uris.forge.make(128.0f), + Resource::Graph::EXTERNAL); + out_properties.put(uris.ingen_canvasY, uris.forge.make(32.0f), + Resource::Graph::EXTERNAL); + + _child_events.push_back( + new Events::CreatePort(_engine, _request_client, -1, _time, + _path.child(Raul::Symbol("notify")), + out_properties)); +} + +bool +CreateGraph::pre_process(PreProcessContext& ctx) +{ + if (_engine.store()->get(_path)) { + return Event::pre_process_done(Status::EXISTS, _path); + } + + if (!_path.is_root()) { + const Raul::Path up(_path.parent()); + if (!(_parent = dynamic_cast<GraphImpl*>(_engine.store()->get(up)))) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, up); + } + } + + const Ingen::URIs& uris = _engine.world()->uris(); + + typedef Properties::const_iterator iterator; + + uint32_t ext_poly = 1; + uint32_t int_poly = 1; + iterator p = _properties.find(uris.ingen_polyphony); + if (p != _properties.end() && p->second.type() == uris.forge.Int) { + int_poly = p->second.get<int32_t>(); + } + + if (int_poly < 1 || int_poly > 128) { + return Event::pre_process_done(Status::INVALID_POLY, _path); + } + + if (!_parent || int_poly == _parent->internal_poly()) { + ext_poly = int_poly; + } + + const Raul::Symbol symbol(_path.is_root() ? "graph" : _path.symbol()); + + // Get graph prototype + iterator t = _properties.find(uris.lv2_prototype); + if (t == _properties.end()) { + t = _properties.find(uris.lv2_prototype); + } + + if (t != _properties.end() && + uris.forge.is_uri(t->second) && + URI::is_valid(uris.forge.str(t->second, false)) && + uri_is_path(URI(uris.forge.str(t->second, false)))) { + // Create a duplicate of an existing graph + const URI prototype(uris.forge.str(t->second, false)); + GraphImpl* ancestor = dynamic_cast<GraphImpl*>( + _engine.store()->get(uri_to_path(prototype))); + if (!ancestor) { + return Event::pre_process_done(Status::PROTOTYPE_NOT_FOUND, prototype); + } else if (!(_graph = dynamic_cast<GraphImpl*>( + ancestor->duplicate(_engine, symbol, _parent)))) { + return Event::pre_process_done(Status::CREATION_FAILED, _path); + } + } else { + // Create a new graph + _graph = new GraphImpl(_engine, symbol, ext_poly, _parent, + _engine.sample_rate(), int_poly); + _graph->add_property(uris.rdf_type, uris.ingen_Graph.urid); + _graph->add_property(uris.rdf_type, + Property(uris.ingen_Block, + Resource::Graph::EXTERNAL)); + } + + _graph->set_properties(_properties); + + if (_parent) { + // Add graph to parent + _parent->add_block(*_graph); + if (_parent->enabled()) { + _graph->enable(); + } + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *_parent); + } + + _graph->activate(*_engine.buffer_factory()); + + // Insert into store and build update to send to clients + _engine.store()->add(_graph); + _update.put_graph(_graph); + for (BlockImpl& block : _graph->blocks()) { + _engine.store()->add(&block); + } + + // Build and pre-process child events to create standard ports + build_child_events(); + for (Event* ev : _child_events) { + ev->pre_process(ctx); + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +CreateGraph::execute(RunContext& context) +{ + if (_graph) { + if (_parent) { + if (_compiled_graph) { + _parent->set_compiled_graph(std::move(_compiled_graph)); + } + } else { + _engine.set_root_graph(_graph); + _graph->enable(); + } + + for (Event* ev : _child_events) { + ev->execute(context); + } + } +} + +void +CreateGraph::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _update.send(*_engine.broadcaster()); + } + + if (_graph) { + for (Event* ev : _child_events) { + ev->post_process(); + } + } +} + +void +CreateGraph::undo(Interface& target) +{ + target.del(_graph->uri()); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/CreateGraph.hpp b/src/server/events/CreateGraph.hpp new file mode 100644 index 00000000..564d553b --- /dev/null +++ b/src/server/events/CreateGraph.hpp @@ -0,0 +1,74 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_CREATEGRAPH_HPP +#define INGEN_EVENTS_CREATEGRAPH_HPP + +#include <list> + +#include "ingen/Resource.hpp" + +#include "CompiledGraph.hpp" +#include "Event.hpp" +#include "events/Get.hpp" + +namespace Ingen { +namespace Server { + +class GraphImpl; + +namespace Events { + +/** Creates a new Graph. + * + * \ingroup engine + */ +class CreateGraph : public Event +{ +public: + CreateGraph(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + const Properties& properties); + + ~CreateGraph(); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + + GraphImpl* graph() { return _graph; } + +private: + void build_child_events(); + + const Raul::Path _path; + Properties _properties; + ClientUpdate _update; + GraphImpl* _graph; + GraphImpl* _parent; + MPtr<CompiledGraph> _compiled_graph; + std::list<Event*> _child_events; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_CREATEGRAPH_HPP diff --git a/src/server/events/CreatePort.cpp b/src/server/events/CreatePort.cpp new file mode 100644 index 00000000..e17b8b01 --- /dev/null +++ b/src/server/events/CreatePort.cpp @@ -0,0 +1,219 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <utility> + +#include "ingen/Atom.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "raul/Array.hpp" +#include "raul/Path.hpp" + +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "CreatePort.hpp" +#include "Driver.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +CreatePort::CreatePort(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + const Properties& properties) + : Event(engine, client, id, timestamp) + , _path(path) + , _port_type(PortType::UNKNOWN) + , _buf_type(0) + , _graph(nullptr) + , _graph_port(nullptr) + , _engine_port(nullptr) + , _properties(properties) +{ + const Ingen::URIs& uris = _engine.world()->uris(); + + typedef Properties::const_iterator Iterator; + typedef std::pair<Iterator, Iterator> Range; + + const Range types = properties.equal_range(uris.rdf_type); + for (Iterator i = types.first; i != types.second; ++i) { + const Atom& type = i->second; + if (type == uris.lv2_AudioPort) { + _port_type = PortType::AUDIO; + } else if (type == uris.lv2_ControlPort) { + _port_type = PortType::CONTROL; + } else if (type == uris.lv2_CVPort) { + _port_type = PortType::CV; + } else if (type == uris.atom_AtomPort) { + _port_type = PortType::ATOM; + } else if (type == uris.lv2_InputPort) { + _flow = Flow::INPUT; + } else if (type == uris.lv2_OutputPort) { + _flow = Flow::OUTPUT; + } + } + + const Range buffer_types = properties.equal_range(uris.atom_bufferType); + for (Iterator i = buffer_types.first; i != buffer_types.second; ++i) { + if (uris.forge.is_uri(i->second)) { + _buf_type = _engine.world()->uri_map().map_uri( + uris.forge.str(i->second, false)); + } + } +} + +bool +CreatePort::pre_process(PreProcessContext& ctx) +{ + if (_port_type == PortType::UNKNOWN) { + return Event::pre_process_done(Status::UNKNOWN_TYPE, _path); + } else if (!_flow) { + return Event::pre_process_done(Status::UNKNOWN_TYPE, _path); + } else if (_path.is_root()) { + return Event::pre_process_done(Status::BAD_URI, _path); + } else if (_engine.store()->get(_path)) { + return Event::pre_process_done(Status::EXISTS, _path); + } + + const Raul::Path parent_path = _path.parent(); + Node* const parent = _engine.store()->get(parent_path); + if (!parent) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, parent_path); + } else if (!(_graph = dynamic_cast<GraphImpl*>(parent))) { + return Event::pre_process_done(Status::INVALID_PARENT, parent_path); + } else if (!_graph->parent() && _engine.activated() && + !_engine.driver()->dynamic_ports()) { + return Event::pre_process_done(Status::CREATION_FAILED, _path); + } + + const URIs& uris = _engine.world()->uris(); + BufferFactory& bufs = *_engine.buffer_factory(); + const uint32_t buf_size = bufs.default_size(_buf_type); + const int32_t old_n_ports = _graph->num_ports_non_rt(); + + typedef Properties::const_iterator PropIter; + + PropIter index_i = _properties.find(uris.lv2_index); + int32_t index = 0; + if (index_i != _properties.end()) { + // Ensure given index is sane and not taken + if (index_i->second.type() != uris.forge.Int) { + return Event::pre_process_done(Status::BAD_REQUEST); + } + + index = index_i->second.get<int32_t>(); + if (_graph->has_port_with_index(index)) { + return Event::pre_process_done(Status::BAD_INDEX); + } + } else { + // No index given, append + index = old_n_ports; + index_i = _properties.emplace(uris.lv2_index, + _engine.world()->forge().make(index)); + } + + const PropIter poly_i = _properties.find(uris.ingen_polyphonic); + const bool polyphonic = (poly_i != _properties.end() && + poly_i->second.type() == uris.forge.Bool && + poly_i->second.get<int32_t>()); + + // Create 0 value if the port requires one + Atom value; + if (_port_type == PortType::CONTROL || _port_type == PortType::CV) { + value = bufs.forge().make(0.0f); + } + + // Create port + _graph_port = new DuplexPort(bufs, _graph, Raul::Symbol(_path.symbol()), + index, + polyphonic, + _port_type, _buf_type, buf_size, + value, _flow == Flow::OUTPUT); + assert((_flow == Flow::OUTPUT && _graph_port->is_output()) || + (_flow == Flow::INPUT && _graph_port->is_input())); + _graph_port->properties().insert(_properties.begin(), _properties.end()); + + _engine.store()->add(_graph_port); + if (_flow == Flow::OUTPUT) { + _graph->add_output(*_graph_port); + } else { + _graph->add_input(*_graph_port); + } + + if (!_graph->parent()) { + _engine_port = _engine.driver()->create_port(_graph_port); + } + + _ports_array = bufs.maid().make_managed<GraphImpl::Ports>( + old_n_ports + 1, nullptr); + + _update = _graph_port->properties(); + + assert(_graph_port->index() == (uint32_t)index_i->second.get<int32_t>()); + assert(_graph->num_ports_non_rt() == (uint32_t)old_n_ports + 1); + assert(_ports_array->size() == _graph->num_ports_non_rt()); + assert(_graph_port->index() < _ports_array->size()); + return Event::pre_process_done(Status::SUCCESS); +} + +void +CreatePort::execute(RunContext& context) +{ + if (_status == Status::SUCCESS) { + const MPtr<GraphImpl::Ports>& old_ports = _graph->external_ports(); + if (old_ports) { + for (uint32_t i = 0; i < old_ports->size(); ++i) { + const auto* const old_port = (*old_ports)[i]; + assert(old_port->index() < _ports_array->size()); + (*_ports_array)[old_port->index()] = (*old_ports)[i]; + } + } + assert(!(*_ports_array)[_graph_port->index()]); + (*_ports_array)[_graph_port->index()] = _graph_port; + _graph->set_external_ports(std::move(_ports_array)); + + if (_engine_port) { + _engine.driver()->add_port(context, _engine_port); + } + } +} + +void +CreatePort::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->put(path_to_uri(_path), _update); + } +} + +void +CreatePort::undo(Interface& target) +{ + target.del(_graph_port->uri()); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/CreatePort.hpp b/src/server/events/CreatePort.hpp new file mode 100644 index 00000000..a2ea7682 --- /dev/null +++ b/src/server/events/CreatePort.hpp @@ -0,0 +1,82 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_CREATEPORT_HPP +#define INGEN_EVENTS_CREATEPORT_HPP + +#include <boost/optional.hpp> + +#include "ingen/Resource.hpp" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "raul/Array.hpp" +#include "raul/Path.hpp" + +#include "BlockImpl.hpp" +#include "Event.hpp" +#include "PortType.hpp" + +namespace Ingen { +namespace Server { + +class DuplexPort; +class EnginePort; +class GraphImpl; +class PortImpl; + +namespace Events { + +/** An event to add a Port to a Graph. + * + * \ingroup engine + */ +class CreatePort : public Event +{ +public: + CreatePort(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + const Properties& properties); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + enum class Flow { + INPUT, + OUTPUT + }; + + Raul::Path _path; + PortType _port_type; + LV2_URID _buf_type; + GraphImpl* _graph; + DuplexPort* _graph_port; + MPtr<BlockImpl::Ports> _ports_array; ///< New external port array for Graph + EnginePort* _engine_port; ///< Driver port if on the root + Properties _properties; + Properties _update; + boost::optional<Flow> _flow; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_CREATEPORT_HPP diff --git a/src/server/events/Delete.cpp b/src/server/events/Delete.cpp new file mode 100644 index 00000000..e8f9582c --- /dev/null +++ b/src/server/events/Delete.cpp @@ -0,0 +1,216 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Store.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "ControlBindings.hpp" +#include "Delete.hpp" +#include "DisconnectAll.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "EnginePort.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Delete::Delete(Engine& engine, + SPtr<Interface> client, + FrameTime timestamp, + const Ingen::Del& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _engine_port(nullptr) + , _disconnect_event(nullptr) +{ + if (uri_is_path(msg.uri)) { + _path = uri_to_path(msg.uri); + } +} + +Delete::~Delete() +{ + delete _disconnect_event; + for (ControlBindings::Binding* b : _removed_bindings) { + delete b; + } +} + +bool +Delete::pre_process(PreProcessContext& ctx) +{ + const Ingen::URIs& uris = _engine.world()->uris(); + if (_path.is_root() || _path == "/control" || _path == "/notify") { + return Event::pre_process_done(Status::NOT_DELETABLE, _path); + } + + _engine.control_bindings()->get_all(_path, _removed_bindings); + + auto iter = _engine.store()->find(_path); + if (iter == _engine.store()->end()) { + return Event::pre_process_done(Status::NOT_FOUND, _path); + } + + if (!(_block = dynamic_ptr_cast<BlockImpl>(iter->second))) { + _port = dynamic_ptr_cast<DuplexPort>(iter->second); + } + + if ((!_block && !_port) || (_port && !_engine.driver()->dynamic_ports())) { + return Event::pre_process_done(Status::NOT_DELETABLE, _path); + } + + GraphImpl* parent = _block ? _block->parent_graph() : _port->parent_graph(); + if (!parent) { + return Event::pre_process_done(Status::INTERNAL_ERROR, _path); + } + + // Take a writer lock while we modify the store + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + _engine.store()->remove(iter, _removed_objects); + + if (_block) { + parent->remove_block(*_block); + _disconnect_event = new DisconnectAll(_engine, parent, _block.get()); + _disconnect_event->pre_process(ctx); + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *parent); + } else if (_port) { + parent->remove_port(*_port); + _disconnect_event = new DisconnectAll(_engine, parent, _port.get()); + _disconnect_event->pre_process(ctx); + + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *parent); + if (parent->enabled()) { + _ports_array = parent->build_ports_array(*_engine.maid()); + assert(_ports_array->size() == parent->num_ports_non_rt()); + + // Adjust port indices if necessary and record changes for later + for (size_t i = 0; i < _ports_array->size(); ++i) { + PortImpl* const port = _ports_array->at(i); + if (port->index() != i) { + _port_index_changes.emplace( + port->path(), std::make_pair(port->index(), i)); + port->remove_property(uris.lv2_index, uris.patch_wildcard); + port->set_property( + uris.lv2_index, + _engine.buffer_factory()->forge().make((int32_t)i)); + } + } + } + + if (!parent->parent()) { + _engine_port = _engine.driver()->get_port(_port->path()); + } + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Delete::execute(RunContext& context) +{ + if (_status != Status::SUCCESS) { + return; + } + + if (_disconnect_event) { + _disconnect_event->execute(context); + } + + if (!_removed_bindings.empty()) { + _engine.control_bindings()->remove(context, _removed_bindings); + } + + GraphImpl* parent = _block ? _block->parent_graph() : nullptr; + if (_port) { + // Adjust port indices if necessary + for (size_t i = 0; i < _ports_array->size(); ++i) { + PortImpl* const port = _ports_array->at(i); + if (port->index() != i) { + port->set_index(context, i); + } + } + + // Replace ports array in graph + parent = _port->parent_graph(); + parent->set_external_ports(std::move(_ports_array)); + + if (_engine_port) { + _engine.driver()->remove_port(context, _engine_port); + } + } + + if (parent && _compiled_graph) { + parent->set_compiled_graph(std::move(_compiled_graph)); + } +} + +void +Delete::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS && (_block || _port)) { + if (_block) { + _block->deactivate(); + } + + _engine.broadcaster()->message(_msg); + } + + if (_engine_port) { + _engine.driver()->unregister_port(*_engine_port); + delete _engine_port; + } +} + +void +Delete::undo(Interface& target) +{ + const Ingen::URIs& uris = _engine.world()->uris(); + Ingen::Forge& forge = _engine.buffer_factory()->forge(); + + auto i = _removed_objects.find(_path); + if (i != _removed_objects.end()) { + // Undo disconnect + if (_disconnect_event) { + _disconnect_event->undo(target); + } + + // Put deleted item back + target.put(_msg.uri, i->second->properties()); + + // Adjust port indices + for (const auto& c : _port_index_changes) { + if (c.first != _msg.uri.path()) { + target.set_property(path_to_uri(c.first), + uris.lv2_index, + forge.make(int32_t(c.second.first))); + } + } + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Delete.hpp b/src/server/events/Delete.hpp new file mode 100644 index 00000000..8b2a0a91 --- /dev/null +++ b/src/server/events/Delete.hpp @@ -0,0 +1,86 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_DELETE_HPP +#define INGEN_EVENTS_DELETE_HPP + +#include <map> +#include <vector> + +#include "ingen/Store.hpp" + +#include "CompiledGraph.hpp" +#include "ControlBindings.hpp" +#include "Event.hpp" +#include "GraphImpl.hpp" + +namespace Raul { +template<typename T> class Array; +} + +namespace Ingen { +namespace Server { + +class BlockImpl; +class DuplexPort; +class EnginePort; +class PortImpl; + +namespace Events { + +class DisconnectAll; + +/** Delete a graph object. + * \ingroup engine + */ +class Delete : public Event +{ +public: + Delete(Engine& engine, + SPtr<Interface> client, + FrameTime timestamp, + const Ingen::Del& msg); + + ~Delete(); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + using IndexChange = std::pair<uint32_t, uint32_t>; + using IndexChanges = std::map<Raul::Path, IndexChange>; + + const Ingen::Del _msg; + Raul::Path _path; + SPtr<BlockImpl> _block; ///< Non-NULL iff a block + SPtr<DuplexPort> _port; ///< Non-NULL iff a port + EnginePort* _engine_port; + MPtr<GraphImpl::Ports> _ports_array; ///< New (external) ports for Graph + MPtr<CompiledGraph> _compiled_graph; ///< Graph's new process order + DisconnectAll* _disconnect_event; + Store::Objects _removed_objects; + IndexChanges _port_index_changes; + + std::vector<ControlBindings::Binding*> _removed_bindings; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_DELETE_HPP diff --git a/src/server/events/Delta.cpp b/src/server/events/Delta.cpp new file mode 100644 index 00000000..b23ae884 --- /dev/null +++ b/src/server/events/Delta.cpp @@ -0,0 +1,670 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <vector> +#include <thread> + +#include "ingen/Log.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIs.hpp" +#include "raul/Maid.hpp" + +#include "Broadcaster.hpp" +#include "ControlBindings.hpp" +#include "CreateBlock.hpp" +#include "CreateGraph.hpp" +#include "CreatePort.hpp" +#include "Delta.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" +#include "PortType.hpp" +#include "SetPortValue.hpp" +#include "events/Get.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Delta::Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Put& msg) + : Event(engine, client, msg.seq, timestamp) + , _create_event(nullptr) + , _subject(msg.uri) + , _properties(msg.properties) + , _object(nullptr) + , _graph(nullptr) + , _binding(nullptr) + , _state(nullptr) + , _context(msg.ctx) + , _type(Type::PUT) + , _block(false) +{ + init(); +} + +Delta::Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Delta& msg) + : Event(engine, client, msg.seq, timestamp) + , _create_event(nullptr) + , _subject(msg.uri) + , _properties(msg.add) + , _remove(msg.remove) + , _object(nullptr) + , _graph(nullptr) + , _binding(nullptr) + , _state(nullptr) + , _context(msg.ctx) + , _type(Type::PATCH) + , _block(false) +{ + init(); +} + +Delta::Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::SetProperty& msg) + : Event(engine, client, msg.seq, timestamp) + , _create_event(nullptr) + , _subject(msg.subject) + , _properties{{msg.predicate, msg.value}} + , _object(nullptr) + , _graph(nullptr) + , _binding(nullptr) + , _state(nullptr) + , _context(msg.ctx) + , _type(Type::SET) + , _block(false) +{ + init(); +} + +Delta::~Delta() +{ + for (auto& s : _set_events) { + delete s; + } + + delete _create_event; +} + +void +Delta::init() +{ + if (_context != Resource::Graph::DEFAULT) { + for (auto& p : _properties) { + p.second.set_context(_context); + } + } + + // Set atomic execution if polyphony is to be changed + const Ingen::URIs& uris = _engine.world()->uris(); + if (_properties.count(uris.ingen_polyphonic) || + _properties.count(uris.ingen_polyphony)) { + _block = true; + } +} + +void +Delta::add_set_event(const char* port_symbol, + const void* value, + uint32_t size, + uint32_t type) +{ + BlockImpl* block = dynamic_cast<BlockImpl*>(_object); + PortImpl* port = block->port_by_symbol(port_symbol); + if (!port) { + _engine.log().warn(fmt("Unknown port `%1%' in state") % port_symbol); + return; + } + + SetPortValue* ev = new SetPortValue( + _engine, _request_client, _request_id, _time, + port, Atom(size, type, value), false, true); + + _set_events.push_back(ev); +} + +static void +s_add_set_event(const char* port_symbol, + void* user_data, + const void* value, + uint32_t size, + uint32_t type) +{ + ((Delta*)user_data)->add_set_event(port_symbol, value, size, type); +} + +static LilvNode* +get_file_node(LilvWorld* lworld, const URIs& uris, const Atom& value) +{ + if (value.type() == uris.atom_Path) { + return lilv_new_file_uri(lworld, nullptr, value.ptr<char>()); + } else if (uris.forge.is_uri(value)) { + const std::string str = uris.forge.str(value, false); + if (str.substr(0, 5) == "file:") { + return lilv_new_uri(lworld, value.ptr<char>()); + } + } + return nullptr; +} + +bool +Delta::pre_process(PreProcessContext& ctx) +{ + const Ingen::URIs& uris = _engine.world()->uris(); + + const bool is_graph_object = uri_is_path(_subject); + const bool is_client = (_subject == "ingen:/clients/this"); + const bool is_engine = (_subject == "ingen:/"); + const bool is_file = (_subject.scheme() == "file"); + + if (_type == Type::PUT && is_file) { + // Ensure type is Preset, the only supported file put + const auto t = _properties.find(uris.rdf_type); + if (t == _properties.end() || t->second != uris.pset_Preset) { + return Event::pre_process_done(Status::BAD_REQUEST, _subject); + } + + // Get "prototype" for preset (node to save state for) + const auto p = _properties.find(uris.lv2_prototype); + if (p == _properties.end()) { + return Event::pre_process_done(Status::BAD_REQUEST, _subject); + } else if (!_engine.world()->forge().is_uri(p->second)) { + return Event::pre_process_done(Status::BAD_REQUEST, _subject); + } + + const URI prot(_engine.world()->forge().str(p->second, false)); + if (!uri_is_path(prot)) { + return Event::pre_process_done(Status::BAD_URI, _subject); + } + + Node* node = _engine.store()->get(uri_to_path(prot)); + if (!node) { + return Event::pre_process_done(Status::NOT_FOUND, prot); + } + + BlockImpl* block = dynamic_cast<BlockImpl*>(node); + if (!block) { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, prot); + } + + if ((_preset = block->save_preset(_subject, _properties))) { + return Event::pre_process_done(Status::SUCCESS); + } else { + return Event::pre_process_done(Status::FAILURE); + } + } + + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + _object = is_graph_object + ? static_cast<Ingen::Resource*>(_engine.store()->get(uri_to_path(_subject))) + : static_cast<Ingen::Resource*>(_engine.block_factory()->plugin(_subject)); + + if (!_object && !is_client && !is_engine && + (!is_graph_object || _type != Type::PUT)) { + return Event::pre_process_done(Status::NOT_FOUND, _subject); + } + + if (is_graph_object && !_object) { + Raul::Path path(uri_to_path(_subject)); + bool is_graph = false, is_block = false, is_port = false, is_output = false; + Ingen::Resource::type(uris, _properties, is_graph, is_block, is_port, is_output); + + if (is_graph) { + _create_event = new CreateGraph( + _engine, _request_client, _request_id, _time, path, _properties); + } else if (is_block) { + _create_event = new CreateBlock( + _engine, _request_client, _request_id, _time, path, _properties); + } else if (is_port) { + _create_event = new CreatePort( + _engine, _request_client, _request_id, _time, + path, _properties); + } + if (_create_event) { + if (_create_event->pre_process(ctx)) { + _object = _engine.store()->get(path); // Get object for setting + } else { + return Event::pre_process_done(Status::CREATION_FAILED, _subject); + } + } else { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, _subject); + } + } + + _types.reserve(_properties.size()); + + NodeImpl* obj = dynamic_cast<NodeImpl*>(_object); + + // Remove any properties removed in delta + for (const auto& r : _remove) { + const URI& key = r.first; + const Atom& value = r.second; + if (key == uris.midi_binding && value == uris.patch_wildcard) { + PortImpl* port = dynamic_cast<PortImpl*>(_object); + if (port) { + _engine.control_bindings()->get_all(port->path(), _removed_bindings); + } + } + if (_object) { + _removed.emplace(key, value); + _object->remove_property(key, value); + } else if (is_engine && key == uris.ingen_loadedBundle) { + LilvWorld* lworld = _engine.world()->lilv_world(); + LilvNode* bundle = get_file_node(lworld, uris, value); + if (bundle) { + for (const auto& p : _engine.block_factory()->plugins()) { + if (p.second->bundle_uri() == lilv_node_as_string(bundle)) { + p.second->set_is_zombie(true); + _update.del(p.second->uri()); + } + } + lilv_world_unload_bundle(lworld, bundle); + _engine.block_factory()->refresh(); + lilv_node_free(bundle); + } else { + _status = Status::BAD_VALUE; + } + } + } + + // Remove all added properties if this is a put or set + if (_object && (_type == Type::PUT || _type == Type::SET)) { + for (auto p = _properties.begin(); + p != _properties.end(); + p = _properties.upper_bound(p->first)) { + for (auto q = _object->properties().find(p->first); + q != _object->properties().end() && q->first == p->first;) { + auto next = q; + ++next; + + if (!_properties.contains(q->first, q->second)) { + const auto r = std::make_pair(q->first, q->second); + _object->properties().erase(q); + _object->on_property_removed(r.first, r.second); + _removed.insert(r); + } + + q = next; + } + } + } + + for (const auto& p : _properties) { + const URI& key = p.first; + const Property& value = p.second; + SpecialType op = SpecialType::NONE; + if (obj) { + Resource& resource = *obj; + if (value != uris.patch_wildcard) { + if (resource.add_property(key, value, value.context())) { + _added.emplace(key, value); + } + } + + BlockImpl* block = nullptr; + PortImpl* port = dynamic_cast<PortImpl*>(_object); + if (port) { + if (key == uris.ingen_broadcast) { + if (value.type() == uris.forge.Bool) { + op = SpecialType::ENABLE_BROADCAST; + } else { + _status = Status::BAD_VALUE_TYPE; + } + } else if (key == uris.ingen_value || key == uris.ingen_activity) { + SetPortValue* ev = new SetPortValue( + _engine, _request_client, _request_id, _time, port, value, + key == uris.ingen_activity); + _set_events.push_back(ev); + } else if (key == uris.midi_binding) { + if (port->is_a(PortType::CONTROL) || port->is_a(PortType::CV)) { + if (value == uris.patch_wildcard) { + _engine.control_bindings()->start_learn(port); + } else if (value.type() == uris.atom_Object) { + op = SpecialType::CONTROL_BINDING; + _binding = new ControlBindings::Binding(); + } else { + _status = Status::BAD_VALUE_TYPE; + } + } else { + _status = Status::BAD_OBJECT_TYPE; + } + } else if (key == uris.lv2_index) { + op = SpecialType::PORT_INDEX; + port->set_property(key, value); + } + } else if ((block = dynamic_cast<BlockImpl*>(_object))) { + if (key == uris.midi_binding && value == uris.patch_wildcard) { + op = SpecialType::CONTROL_BINDING; // Internal block learn + } else if (key == uris.ingen_enabled) { + if (value.type() == uris.forge.Bool) { + op = SpecialType::ENABLE; + } else { + _status = Status::BAD_VALUE_TYPE; + } + } else if (key == uris.pset_preset) { + URI uri; + if (uris.forge.is_uri(value)) { + const std::string uri_str = uris.forge.str(value, false); + if (URI::is_valid(uri_str)) { + uri = URI(uri_str); + } + } else if (value.type() == uris.forge.Path) { + uri = URI(FilePath(value.ptr<char>())); + } + + if (!uri.empty()) { + op = SpecialType::PRESET; + if ((_state = block->load_preset(uri))) { + lilv_state_emit_port_values( + _state, s_add_set_event, this); + } else { + _engine.log().warn(fmt("Failed to load preset <%1%>\n") % uri); + } + } else { + _status = Status::BAD_VALUE; + } + } + } + + if ((_graph = dynamic_cast<GraphImpl*>(_object))) { + if (key == uris.ingen_enabled) { + if (value.type() == uris.forge.Bool) { + op = SpecialType::ENABLE; + // FIXME: defer this until all other metadata has been processed + if (value.get<int32_t>() && !_graph->enabled()) { + if (!(_compiled_graph = compile(*_engine.maid(), *_graph))) { + _status = Status::COMPILATION_FAILED; + } + } + } else { + _status = Status::BAD_VALUE_TYPE; + } + } else if (key == uris.ingen_polyphony) { + if (value.type() == uris.forge.Int) { + if (value.get<int32_t>() < 1 || value.get<int32_t>() > 128) { + _status = Status::INVALID_POLY; + } else { + op = SpecialType::POLYPHONY; + _graph->prepare_internal_poly( + *_engine.buffer_factory(), value.get<int32_t>()); + } + } else { + _status = Status::BAD_VALUE_TYPE; + } + } + } + + if (!_create_event && key == uris.ingen_polyphonic) { + GraphImpl* parent = dynamic_cast<GraphImpl*>(obj->parent()); + if (!parent) { + _status = Status::BAD_OBJECT_TYPE; + } else if (value.type() != uris.forge.Bool) { + _status = Status::BAD_VALUE_TYPE; + } else { + op = SpecialType::POLYPHONIC; + obj->set_property(key, value, value.context()); + BlockImpl* block = dynamic_cast<BlockImpl*>(obj); + if (block) { + block->set_polyphonic(value.get<int32_t>()); + } + if (value.get<int32_t>()) { + obj->prepare_poly(*_engine.buffer_factory(), parent->internal_poly()); + } else { + obj->prepare_poly(*_engine.buffer_factory(), 1); + } + } + } + } else if (is_client && key == uris.ingen_broadcast) { + _engine.broadcaster()->set_broadcast( + _request_client, value.get<int32_t>()); + } else if (is_engine && key == uris.ingen_loadedBundle) { + LilvWorld* lworld = _engine.world()->lilv_world(); + LilvNode* bundle = get_file_node(lworld, uris, value); + if (bundle) { + lilv_world_load_bundle(lworld, bundle); + const std::set<PluginImpl*> new_plugins = + _engine.block_factory()->refresh(); + + for (PluginImpl* p : new_plugins) { + if (p->bundle_uri() == lilv_node_as_string(bundle)) { + _update.put_plugin(p); + } + } + lilv_node_free(bundle); + } else { + _status = Status::BAD_VALUE; + } + } + + if (_status != Status::NOT_PREPARED) { + break; + } + + _types.push_back(op); + } + + for (auto& s : _set_events) { + s->pre_process(ctx); + } + + return Event::pre_process_done( + _status == Status::NOT_PREPARED ? Status::SUCCESS : _status, + _subject); +} + +void +Delta::execute(RunContext& context) +{ + if (_status != Status::SUCCESS || _preset) { + return; + } + + const Ingen::URIs& uris = _engine.world()->uris(); + + if (_create_event) { + _create_event->set_time(_time); + _create_event->execute(context); + } + + for (auto& s : _set_events) { + s->set_time(_time); + s->execute(context); + } + + if (!_removed_bindings.empty()) { + _engine.control_bindings()->remove(context, _removed_bindings); + } + + NodeImpl* const object = dynamic_cast<NodeImpl*>(_object); + BlockImpl* const block = dynamic_cast<BlockImpl*>(_object); + PortImpl* const port = dynamic_cast<PortImpl*>(_object); + + std::vector<SpecialType>::const_iterator t = _types.begin(); + for (const auto& p : _properties) { + const URI& key = p.first; + const Atom& value = p.second; + switch (*t++) { + case SpecialType::ENABLE_BROADCAST: + if (port) { + port->enable_monitoring(value.get<int32_t>()); + } + break; + case SpecialType::ENABLE: + if (_graph) { + if (value.get<int32_t>()) { + if (_compiled_graph) { + _graph->set_compiled_graph(std::move(_compiled_graph)); + } + _graph->enable(); + } else { + _graph->disable(context); + } + } else if (block) { + block->set_enabled(value.get<int32_t>()); + } + break; + case SpecialType::POLYPHONIC: { + GraphImpl* parent = reinterpret_cast<GraphImpl*>(object->parent()); + if (value.get<int32_t>()) { + object->apply_poly(context, parent->internal_poly_process()); + } else { + object->apply_poly(context, 1); + } + } break; + case SpecialType::POLYPHONY: + if (!_graph->apply_internal_poly(context, + *_engine.buffer_factory(), + *_engine.maid(), + value.get<int32_t>())) { + _status = Status::INTERNAL_ERROR; + } + break; + case SpecialType::PORT_INDEX: + if (port) { + port->set_index(context, value.get<int32_t>()); + } + break; + case SpecialType::CONTROL_BINDING: + if (port) { + if (!_engine.control_bindings()->set_port_binding(context, port, _binding, value)) { + _status = Status::BAD_VALUE; + } + } else if (block) { + if (uris.ingen_Internal == block->plugin_impl()->type()) { + block->learn(); + } + } + break; + case SpecialType::PRESET: + block->set_enabled(false); + break; + case SpecialType::NONE: + if (port) { + if (key == uris.lv2_minimum) { + port->set_minimum(value); + } else if (key == uris.lv2_maximum) { + port->set_maximum(value); + } + } + case SpecialType::LOADED_BUNDLE: + break; + } + } +} + +void +Delta::post_process() +{ + if (_state) { + BlockImpl* block = dynamic_cast<BlockImpl*>(_object); + if (block) { + block->apply_state(_engine.sync_worker(), _state); + block->set_enabled(true); + } + lilv_state_free(_state); + } + + Broadcaster::Transfer t(*_engine.broadcaster()); + + if (_create_event) { + _create_event->post_process(); + if (_create_event->status() != Status::SUCCESS) { + return; // Creation failed, nothing else to do + } + } + + for (auto& s : _set_events) { + if (s->synthetic() || s->status() != Status::SUCCESS) { + s->post_process(); // Set failed, report error + } + } + + if (respond() == Status::SUCCESS) { + _update.send(*_engine.broadcaster()); + + switch (_type) { + case Type::SET: + /* Kludge to avoid feedback for set events only. The GUI + depends on put responses to e.g. initially place blocks. + Some more sensible way of controlling this is needed. */ + if (_mode == Mode::NORMAL) { + _engine.broadcaster()->set_ignore_client(_request_client); + } + _engine.broadcaster()->set_property( + _subject, + _properties.begin()->first, + _properties.begin()->second); + if (_mode == Mode::NORMAL) { + _engine.broadcaster()->clear_ignore_client(); + } + break; + case Type::PUT: + if (_type == Type::PUT && _subject.scheme() == "file") { + // Preset save + ClientUpdate response; + response.put(_preset->uri(), _preset->properties()); + response.send(*_engine.broadcaster()); + } else { + // Graph object put + _engine.broadcaster()->put(_subject, _properties, _context); + } + break; + case Type::PATCH: + _engine.broadcaster()->delta(_subject, _remove, _properties, _context); + break; + } + } +} + +void +Delta::undo(Interface& target) +{ + if (_create_event) { + _create_event->undo(target); + } else if (_type == Type::PATCH) { + target.delta(_subject, _added, _removed, _context); + } else if (_type == Type::SET || _type == Type::PUT) { + if (_removed.size() == 1) { + target.set_property(_subject, + _removed.begin()->first, + _removed.begin()->second, + _context); + } else if (_removed.empty()) { + target.delta(_subject, _added, {}, _context); + } else { + target.put(_subject, _removed, _context); + } + } +} + +Event::Execution +Delta::get_execution() const +{ + return _block ? Execution::ATOMIC : Execution::NORMAL; +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Delta.hpp b/src/server/events/Delta.hpp new file mode 100644 index 00000000..af337b57 --- /dev/null +++ b/src/server/events/Delta.hpp @@ -0,0 +1,133 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_DELTA_HPP +#define INGEN_EVENTS_DELTA_HPP + +#include <vector> + +#include <boost/optional.hpp> + +#include "lilv/lilv.h" + +#include "CompiledGraph.hpp" +#include "ControlBindings.hpp" +#include "Event.hpp" +#include "PluginImpl.hpp" + +namespace Ingen { + +class Resource; + +namespace Server { + +class Engine; +class GraphImpl; +class RunContext; + +namespace Events { + +class SetPortValue; + +/** Set properties of a graph object. + * \ingroup engine + */ +class Delta : public Event +{ +public: + Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Put& msg); + + Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Delta& msg); + + Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::SetProperty& msg); + + ~Delta(); + + void add_set_event(const char* port_symbol, + const void* value, + uint32_t size, + uint32_t type); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + + Execution get_execution() const; + +private: + enum class Type { + SET, + PUT, + PATCH + }; + + enum class SpecialType { + NONE, + ENABLE, + ENABLE_BROADCAST, + POLYPHONY, + POLYPHONIC, + PORT_INDEX, + CONTROL_BINDING, + PRESET, + LOADED_BUNDLE + }; + + typedef std::vector<SetPortValue*> SetEvents; + + void init(); + + Event* _create_event; + SetEvents _set_events; + std::vector<SpecialType> _types; + std::vector<SpecialType> _remove_types; + URI _subject; + Properties _properties; + Properties _remove; + ClientUpdate _update; + Ingen::Resource* _object; + GraphImpl* _graph; + MPtr<CompiledGraph> _compiled_graph; + ControlBindings::Binding* _binding; + LilvState* _state; + Resource::Graph _context; + Type _type; + + Properties _added; + Properties _removed; + + std::vector<ControlBindings::Binding*> _removed_bindings; + + boost::optional<Resource> _preset; + + bool _block; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_DELTA_HPP diff --git a/src/server/events/Disconnect.cpp b/src/server/events/Disconnect.cpp new file mode 100644 index 00000000..4553c8a2 --- /dev/null +++ b/src/server/events/Disconnect.cpp @@ -0,0 +1,224 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <set> + +#include "ingen/Store.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "ArcImpl.hpp" +#include "Broadcaster.hpp" +#include "Buffer.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" +#include "events/Disconnect.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Disconnect::Disconnect(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Disconnect& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _graph(nullptr) + , _impl(nullptr) +{ +} + +Disconnect::~Disconnect() +{ + delete _impl; +} + +Disconnect::Impl::Impl(Engine& e, + GraphImpl* graph, + PortImpl* t, + InputPort* h) + : _engine(e) + , _tail(t) + , _head(h) + , _arc(graph->remove_arc(_tail, _head)) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + BlockImpl* const tail_block = _tail->parent_block(); + BlockImpl* const head_block = _head->parent_block(); + + // Remove tail from head's providers + auto hp = head_block->providers().find(tail_block); + if (hp != head_block->providers().end()) { + head_block->providers().erase(hp); + } + + // Remove head from tail's providers + auto td = tail_block->dependants().find(head_block); + if (td != tail_block->dependants().end()) { + tail_block->dependants().erase(td); + } + + _head->decrement_num_arcs(); + + if (_head->num_arcs() == 0) { + if (!_head->is_driver_port()) { + BufferFactory& bufs = *_engine.buffer_factory(); + _voices = bufs.maid().make_managed<PortImpl::Voices>(_head->poly()); + _head->pre_get_buffers(bufs, _voices, _head->poly()); + + if (_head->is_a(PortType::CONTROL) || + _head->is_a(PortType::CV)) { + // Reset buffer to control value + const float value = _head->value().get<float>(); + for (uint32_t i = 0; i < _voices->size(); ++i) { + Buffer* buf = _voices->at(i).buffer.get(); + buf->set_block(value, 0, e.block_length()); + } + } else { + for (uint32_t i = 0; i < _voices->size(); ++i) { + _voices->at(i).buffer->clear(); + } + } + } + } +} + +bool +Disconnect::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + if (_msg.tail.parent().parent() != _msg.head.parent().parent() + && _msg.tail.parent() != _msg.head.parent().parent() + && _msg.tail.parent().parent() != _msg.head.parent()) { + return Event::pre_process_done(Status::PARENT_DIFFERS, _msg.head); + } + + PortImpl* tail = dynamic_cast<PortImpl*>(_engine.store()->get(_msg.tail)); + if (!tail) { + return Event::pre_process_done(Status::PORT_NOT_FOUND, _msg.tail); + } + + PortImpl* head = dynamic_cast<PortImpl*>(_engine.store()->get(_msg.head)); + if (!head) { + return Event::pre_process_done(Status::PORT_NOT_FOUND, _msg.head); + } + + BlockImpl* const tail_block = tail->parent_block(); + BlockImpl* const head_block = head->parent_block(); + + if (tail_block->parent_graph() != head_block->parent_graph()) { + // Arc to a graph port from inside the graph + assert(tail_block->parent() == head_block || head_block->parent() == tail_block); + if (tail_block->parent() == head_block) { + _graph = dynamic_cast<GraphImpl*>(head_block); + } else { + _graph = dynamic_cast<GraphImpl*>(tail_block); + } + } else if (tail_block == head_block && dynamic_cast<GraphImpl*>(tail_block)) { + // Arc from a graph input to a graph output (pass through) + _graph = dynamic_cast<GraphImpl*>(tail_block); + } else { + // Normal arc between blocks with the same parent + _graph = tail_block->parent_graph(); + } + + if (!_graph) { + return Event::pre_process_done(Status::INTERNAL_ERROR, _msg.head); + } else if (!_graph->has_arc(tail, head)) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.head); + } + + if (tail_block == nullptr || head_block == nullptr) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, _msg.head); + } + + _impl = new Impl(_engine, + _graph, + dynamic_cast<PortImpl*>(tail), + dynamic_cast<InputPort*>(head)); + + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *_graph); + + return Event::pre_process_done(Status::SUCCESS); +} + +bool +Disconnect::Impl::execute(RunContext& context, bool set_head_buffers) +{ + if (!_arc) { + return false; + } + + _head->remove_arc(*_arc.get()); + if (_head->is_driver_port()) { + return true; + } + + if (set_head_buffers) { + if (_voices) { + _head->set_voices(context, std::move(_voices)); + } else { + _head->setup_buffers(context, *_engine.buffer_factory(), _head->poly()); + } + _head->connect_buffers(); + } else { + _head->recycle_buffers(); + } + + return true; +} + +void +Disconnect::execute(RunContext& context) +{ + if (_status == Status::SUCCESS) { + if (_impl->execute(context, true)) { + if (_compiled_graph) { + _graph->set_compiled_graph(std::move(_compiled_graph)); + } + } else { + _status = Status::NOT_FOUND; + } + } +} + +void +Disconnect::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + } +} + +void +Disconnect::undo(Interface& target) +{ + target.connect(_msg.tail, _msg.head); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Disconnect.hpp b/src/server/events/Disconnect.hpp new file mode 100644 index 00000000..44290d7c --- /dev/null +++ b/src/server/events/Disconnect.hpp @@ -0,0 +1,87 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_DISCONNECT_HPP +#define INGEN_EVENTS_DISCONNECT_HPP + +#include "raul/Path.hpp" + +#include "BufferFactory.hpp" +#include "CompiledGraph.hpp" +#include "Event.hpp" +#include "GraphImpl.hpp" +#include "types.hpp" + +namespace Raul { +template <typename T> class Array; +} + +namespace Ingen { +namespace Server { + +class InputPort; +class PortImpl; + +namespace Events { + +/** Remove an Arc between two Ports. + * + * \ingroup engine + */ +class Disconnect : public Event +{ +public: + Disconnect(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Disconnect& msg); + + ~Disconnect(); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + + class Impl { + public: + Impl(Engine& e, GraphImpl* graph, PortImpl* t, InputPort* h); + + bool execute(RunContext& context, bool set_head_buffers); + + inline PortImpl* tail() { return _tail; } + inline InputPort* head() { return _head; } + + private: + Engine& _engine; + PortImpl* _tail; + InputPort* _head; + SPtr<ArcImpl> _arc; + MPtr<PortImpl::Voices> _voices; + }; + +private: + const Ingen::Disconnect _msg; + GraphImpl* _graph; + Impl* _impl; + MPtr<CompiledGraph> _compiled_graph; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_DISCONNECT_HPP diff --git a/src/server/events/DisconnectAll.cpp b/src/server/events/DisconnectAll.cpp new file mode 100644 index 00000000..11311d12 --- /dev/null +++ b/src/server/events/DisconnectAll.cpp @@ -0,0 +1,176 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <set> + +#include "ingen/Store.hpp" +#include "raul/Array.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "ArcImpl.hpp" +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" +#include "events/Disconnect.hpp" +#include "events/DisconnectAll.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +DisconnectAll::DisconnectAll(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::DisconnectAll& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _parent(nullptr) + , _block(nullptr) + , _port(nullptr) + , _deleting(false) +{ +} + +/** Internal version for use by other events. + */ +DisconnectAll::DisconnectAll(Engine& engine, + GraphImpl* parent, + Node* object) + : Event(engine) + , _msg{0, parent->path(), object->path()} + , _parent(parent) + , _block(dynamic_cast<BlockImpl*>(object)) + , _port(dynamic_cast<PortImpl*>(object)) + , _deleting(true) +{ +} + +DisconnectAll::~DisconnectAll() +{ + for (auto& i : _impls) { + delete i; + } +} + +bool +DisconnectAll::pre_process(PreProcessContext& ctx) +{ + std::unique_lock<Store::Mutex> lock(_engine.store()->mutex(), + std::defer_lock); + + if (!_deleting) { + lock.lock(); + + _parent = dynamic_cast<GraphImpl*>(_engine.store()->get(_msg.graph)); + if (!_parent) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, + _msg.graph); + } + + NodeImpl* const object = dynamic_cast<NodeImpl*>( + _engine.store()->get(_msg.path)); + if (!object) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.path); + } + + if (object->parent_graph() != _parent + && object->parent()->parent_graph() != _parent) { + return Event::pre_process_done(Status::INVALID_PARENT, _msg.graph); + } + + // Only one of these will succeed + _block = dynamic_cast<BlockImpl*>(object); + _port = dynamic_cast<PortImpl*>(object); + + if (!_block && !_port) { + return Event::pre_process_done(Status::INTERNAL_ERROR, _msg.path); + } + } + + // Find set of arcs to remove + std::set<ArcImpl*> to_remove; + for (const auto& a : _parent->arcs()) { + ArcImpl* const arc = (ArcImpl*)a.second.get(); + if (_block) { + if (arc->tail()->parent_block() == _block + || arc->head()->parent_block() == _block) { + to_remove.insert(arc); + } + } else if (_port) { + if (arc->tail() == _port || arc->head() == _port) { + to_remove.insert(arc); + } + } + } + + // Create disconnect events (which erases from _parent->arcs()) + for (const auto& a : to_remove) { + _impls.push_back(new Disconnect::Impl( + _engine, _parent, + dynamic_cast<PortImpl*>(a->tail()), + dynamic_cast<InputPort*>(a->head()))); + } + + if (!_deleting && ctx.must_compile(*_parent)) { + if (!(_compiled_graph = compile(*_engine.maid(), *_parent))) { + return Event::pre_process_done(Status::COMPILATION_FAILED); + } + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +DisconnectAll::execute(RunContext& context) +{ + if (_status == Status::SUCCESS) { + for (auto& i : _impls) { + i->execute(context, + !_deleting || (i->head()->parent_block() != _block)); + } + } + + if (_compiled_graph) { + _parent->set_compiled_graph(std::move(_compiled_graph)); + } +} + +void +DisconnectAll::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + } +} + +void +DisconnectAll::undo(Interface& target) +{ + for (auto& i : _impls) { + target.connect(i->tail()->path(), i->head()->path()); + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/DisconnectAll.hpp b/src/server/events/DisconnectAll.hpp new file mode 100644 index 00000000..947e538f --- /dev/null +++ b/src/server/events/DisconnectAll.hpp @@ -0,0 +1,78 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_DISCONNECTALL_HPP +#define INGEN_EVENTS_DISCONNECTALL_HPP + +#include <list> + +#include "raul/Path.hpp" + +#include "CompiledGraph.hpp" +#include "Disconnect.hpp" +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; +class PortImpl; + +namespace Events { + +class Disconnect; + +/** An event to disconnect all connections to a Block. + * + * \ingroup engine + */ +class DisconnectAll : public Event +{ +public: + DisconnectAll(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::DisconnectAll& msg); + + DisconnectAll(Engine& engine, + GraphImpl* parent, + Node* object); + + ~DisconnectAll(); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + typedef std::list<Disconnect::Impl*> Impls; + + const Ingen::DisconnectAll _msg; + GraphImpl* _parent; + BlockImpl* _block; + PortImpl* _port; + Impls _impls; + MPtr<CompiledGraph> _compiled_graph; + bool _deleting; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_DISCONNECTALL_HPP diff --git a/src/server/events/Get.cpp b/src/server/events/Get.cpp new file mode 100644 index 00000000..e53e8c41 --- /dev/null +++ b/src/server/events/Get.cpp @@ -0,0 +1,111 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <utility> + +#include "ingen/Interface.hpp" +#include "ingen/Node.hpp" +#include "ingen/Store.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "Get.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Get::Get(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Get& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _object(nullptr) + , _plugin(nullptr) +{} + +bool +Get::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + const auto& uri = _msg.subject; + if (uri == "ingen:/plugins") { + _plugins = _engine.block_factory()->plugins(); + return Event::pre_process_done(Status::SUCCESS); + } else if (uri == "ingen:/engine") { + return Event::pre_process_done(Status::SUCCESS); + } else if (uri_is_path(uri)) { + if ((_object = _engine.store()->get(uri_to_path(uri)))) { + const BlockImpl* block = nullptr; + const GraphImpl* graph = nullptr; + const PortImpl* port = nullptr; + if ((graph = dynamic_cast<const GraphImpl*>(_object))) { + _response.put_graph(graph); + } else if ((block = dynamic_cast<const BlockImpl*>(_object))) { + _response.put_block(block); + } else if ((port = dynamic_cast<const PortImpl*>(_object))) { + _response.put_port(port); + } else { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, uri); + } + return Event::pre_process_done(Status::SUCCESS); + } + return Event::pre_process_done(Status::NOT_FOUND, uri); + } else if ((_plugin = _engine.block_factory()->plugin(uri))) { + _response.put_plugin(_plugin); + return Event::pre_process_done(Status::SUCCESS); + } else { + return Event::pre_process_done(Status::NOT_FOUND, uri); + } +} + +void +Get::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS && _request_client) { + if (_msg.subject == "ingen:/plugins") { + _engine.broadcaster()->send_plugins_to(_request_client.get(), _plugins); + } else if (_msg.subject == "ingen:/engine") { + // TODO: Keep a proper RDF model of the engine + URIs& uris = _engine.world()->uris(); + Properties props = { + { uris.param_sampleRate, + uris.forge.make(int32_t(_engine.sample_rate())) }, + { uris.bufsz_maxBlockLength, + uris.forge.make(int32_t(_engine.block_length())) }, + { uris.ingen_numThreads, + uris.forge.make(int32_t(_engine.n_threads())) } }; + + const Properties load_props = _engine.load_properties(); + props.insert(load_props.begin(), load_props.end()); + _request_client->put(URI("ingen:/engine"), props); + } else { + _response.send(*_request_client); + } + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Get.hpp b/src/server/events/Get.hpp new file mode 100644 index 00000000..7392550f --- /dev/null +++ b/src/server/events/Get.hpp @@ -0,0 +1,65 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_GET_HPP +#define INGEN_EVENTS_GET_HPP + +#include <vector> + +#include "BlockFactory.hpp" +#include "ClientUpdate.hpp" +#include "Event.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; +class PluginImpl; +class PortImpl; + +namespace Events { + +/** A request from a client to send an object. + * + * \ingroup engine + */ +class Get : public Event +{ +public: + Get(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Get& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context) {} + void post_process(); + +private: + const Ingen::Get _msg; + const Node* _object; + PluginImpl* _plugin; + BlockFactory::Plugins _plugins; + ClientUpdate _response; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_GET_HPP diff --git a/src/server/events/Mark.cpp b/src/server/events/Mark.cpp new file mode 100644 index 00000000..3c0dfaaf --- /dev/null +++ b/src/server/events/Mark.cpp @@ -0,0 +1,112 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "Engine.hpp" +#include "PreProcessContext.hpp" +#include "UndoStack.hpp" +#include "events/Mark.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Mark::Mark(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::BundleBegin& msg) + : Event(engine, client, msg.seq, timestamp) + , _type(Type::BUNDLE_BEGIN) + , _depth(0) +{} + +Mark::Mark(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::BundleEnd& msg) + : Event(engine, client, msg.seq, timestamp) + , _type(Type::BUNDLE_END) + , _depth(0) +{} + +bool +Mark::pre_process(PreProcessContext& ctx) +{ + const UPtr<UndoStack>& stack = ((_mode == Mode::UNDO) + ? _engine.redo_stack() + : _engine.undo_stack()); + + switch (_type) { + case Type::BUNDLE_BEGIN: + ctx.set_in_bundle(true); + _depth = stack->start_entry(); + break; + case Type::BUNDLE_END: + _depth = stack->finish_entry(); + ctx.set_in_bundle(false); + if (!ctx.dirty_graphs().empty()) { + for (GraphImpl* g : ctx.dirty_graphs()) { + MPtr<CompiledGraph> cg = compile(*_engine.maid(), *g); + if (cg) { + _compiled_graphs.emplace(g, std::move(cg)); + } + } + ctx.dirty_graphs().clear(); + } + break; + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Mark::execute(RunContext& context) +{ + for (auto& g : _compiled_graphs) { + g.first->set_compiled_graph(std::move(g.second)); + } +} + +void +Mark::post_process() +{ + respond(); +} + +Event::Execution +Mark::get_execution() const +{ + if (!_engine.atomic_bundles()) { + return Execution::NORMAL; + } + + switch (_type) { + case Type::BUNDLE_BEGIN: + if (_depth == 1) { + return Execution::BLOCK; + } + break; + case Type::BUNDLE_END: + if (_depth == 0) { + return Execution::UNBLOCK; + } + break; + } + return Execution::NORMAL; +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Mark.hpp b/src/server/events/Mark.hpp new file mode 100644 index 00000000..eaeb9332 --- /dev/null +++ b/src/server/events/Mark.hpp @@ -0,0 +1,69 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_MARK_HPP +#define INGEN_EVENTS_MARK_HPP + +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class Engine; + +namespace Events { + +/** Delineate the start or end of a bundle of events. + * + * This is used to mark the ends of an undo transaction, so a single undo can + * undo the effects of many events (such as a paste or a graph load). + * + * \ingroup engine + */ +class Mark : public Event +{ +public: + Mark(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::BundleBegin& msg); + + Mark(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::BundleEnd& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + + Execution get_execution() const; + +private: + enum class Type { BUNDLE_BEGIN, BUNDLE_END }; + + typedef std::map<GraphImpl*, MPtr<CompiledGraph>> CompiledGraphs; + + CompiledGraphs _compiled_graphs; + Type _type; + int _depth; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_MARK_HPP diff --git a/src/server/events/Move.cpp b/src/server/events/Move.cpp new file mode 100644 index 00000000..b0935675 --- /dev/null +++ b/src/server/events/Move.cpp @@ -0,0 +1,91 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Store.hpp" +#include "raul/Path.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "EnginePort.hpp" +#include "GraphImpl.hpp" +#include "events/Move.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Move::Move(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Move& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) +{ +} + +bool +Move::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + if (!_msg.old_path.parent().is_parent_of(_msg.new_path)) { + return Event::pre_process_done(Status::PARENT_DIFFERS, _msg.new_path); + } + + const Store::iterator i = _engine.store()->find(_msg.old_path); + if (i == _engine.store()->end()) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.old_path); + } + + if (_engine.store()->find(_msg.new_path) != _engine.store()->end()) { + return Event::pre_process_done(Status::EXISTS, _msg.new_path); + } + + EnginePort* eport = _engine.driver()->get_port(_msg.old_path); + if (eport) { + _engine.driver()->rename_port(_msg.old_path, _msg.new_path); + } + + _engine.store()->rename(i, _msg.new_path); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Move::execute(RunContext& context) +{ +} + +void +Move::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + } +} + +void +Move::undo(Interface& target) +{ + target.move(_msg.new_path, _msg.old_path); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Move.hpp b/src/server/events/Move.hpp new file mode 100644 index 00000000..459d2709 --- /dev/null +++ b/src/server/events/Move.hpp @@ -0,0 +1,57 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_MOVE_HPP +#define INGEN_EVENTS_MOVE_HPP + +#include "ingen/Store.hpp" +#include "raul/Path.hpp" + +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class GraphImpl; +class PortImpl; + +namespace Events { + +/** Move a graph object to a new path. + * \ingroup engine + */ +class Move : public Event +{ +public: + Move(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Move& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + const Ingen::Move _msg; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_MOVE_HPP diff --git a/src/server/events/SetPortValue.cpp b/src/server/events/SetPortValue.cpp new file mode 100644 index 00000000..62f2def6 --- /dev/null +++ b/src/server/events/SetPortValue.cpp @@ -0,0 +1,139 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/LV2Features.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "Buffer.hpp" +#include "ControlBindings.hpp" +#include "Engine.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "SetPortValue.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +/** Internal */ +SetPortValue::SetPortValue(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + PortImpl* port, + const Atom& value, + bool activity, + bool synthetic) + : Event(engine, client, id, timestamp) + , _port(port) + , _value(value) + , _activity(activity) + , _synthetic(synthetic) +{ +} + +bool +SetPortValue::pre_process(PreProcessContext& ctx) +{ + Ingen::URIs& uris = _engine.world()->uris(); + if (_port->is_output()) { + return Event::pre_process_done(Status::DIRECTION_MISMATCH, _port->path()); + } + + if (!_activity) { + // Set value metadata (does not affect buffers) + _port->set_value(_value); + _port->set_property(_engine.world()->uris().ingen_value, _value); + } + + _binding = _engine.control_bindings()->port_binding(_port); + + if (_port->buffer_type() == uris.atom_Sequence) { + _buffer = _engine.buffer_factory()->get_buffer( + _port->buffer_type(), + _value.type() == uris.atom_Float ? _value.type() : 0, + _engine.buffer_factory()->default_size(_port->buffer_type())); + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +SetPortValue::execute(RunContext& context) +{ + assert(_time >= context.start() && _time <= context.end()); + apply(context); + _engine.control_bindings()->port_value_changed(context, _port, _binding, _value); +} + +void +SetPortValue::apply(RunContext& context) +{ + if (_status != Status::SUCCESS) { + return; + } + + Ingen::URIs& uris = _engine.world()->uris(); + Buffer* buf = _port->buffer(0).get(); + + if (_buffer) { + if (_port->user_buffer(context)) { + buf = _port->user_buffer(context).get(); + } else { + _port->set_user_buffer(context, _buffer); + buf = _buffer.get(); + } + } + + if (buf->type() == uris.atom_Sound || buf->type() == uris.atom_Float) { + if (_value.type() == uris.forge.Float) { + _port->set_control_value(context, _time, _value.get<float>()); + } else { + _status = Status::TYPE_MISMATCH; + } + } else if (buf->type() == uris.atom_Sequence) { + if (!buf->append_event(_time - context.start(), + _value.size(), + _value.type(), + (const uint8_t*)_value.get_body())) { + _status = Status::NO_SPACE; + } + } else if (buf->type() == uris.atom_URID) { + buf->get<LV2_Atom_URID>()->body = _value.get<int32_t>(); + } else { + _status = Status::BAD_VALUE_TYPE; + } +} + +void +SetPortValue::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS && !_activity) { + _engine.broadcaster()->set_property( + _port->uri(), + _engine.world()->uris().ingen_value, + _value); + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/SetPortValue.hpp b/src/server/events/SetPortValue.hpp new file mode 100644 index 00000000..4df60019 --- /dev/null +++ b/src/server/events/SetPortValue.hpp @@ -0,0 +1,71 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_SETPORTVALUE_HPP +#define INGEN_EVENTS_SETPORTVALUE_HPP + +#include "ingen/Atom.hpp" + +#include "BufferRef.hpp" +#include "ControlBindings.hpp" +#include "Event.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class PortImpl; + +namespace Events { + +/** An event to change the value of a port. + * + * \ingroup engine + */ +class SetPortValue : public Event +{ +public: + SetPortValue(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + PortImpl* port, + const Atom& value, + bool activity, + bool synthetic = false); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + + bool synthetic() const { return _synthetic; } + +private: + void apply(RunContext& context); + + PortImpl* _port; + const Atom _value; + BufferRef _buffer; + ControlBindings::Key _binding; + bool _activity; + bool _synthetic; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_SETPORTVALUE_HPP diff --git a/src/server/events/Undo.cpp b/src/server/events/Undo.cpp new file mode 100644 index 00000000..e06a5951 --- /dev/null +++ b/src/server/events/Undo.cpp @@ -0,0 +1,85 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/AtomReader.hpp" + +#include "Engine.hpp" +#include "EventWriter.hpp" +#include "Undo.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Undo::Undo(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Undo& msg) + : Event(engine, client, msg.seq, timestamp) + , _is_redo(false) +{} + +Undo::Undo(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Redo& msg) + : Event(engine, client, msg.seq, timestamp) + , _is_redo(true) +{} + +bool +Undo::pre_process(PreProcessContext& ctx) +{ + const UPtr<UndoStack>& stack = _is_redo ? _engine.redo_stack() : _engine.undo_stack(); + const Event::Mode mode = _is_redo ? Event::Mode::REDO : Event::Mode::UNDO; + + if (stack->empty()) { + return Event::pre_process_done(Status::NOT_FOUND); + } + + const Event::Mode orig_mode = _engine.event_writer()->get_event_mode(); + _entry = stack->pop(); + _engine.event_writer()->set_event_mode(mode); + if (_entry.events.size() > 1) { + _engine.interface()->bundle_begin(); + } + + for (const LV2_Atom* ev : _entry.events) { + _engine.atom_interface()->write(ev); + } + + if (_entry.events.size() > 1) { + _engine.interface()->bundle_end(); + } + _engine.event_writer()->set_event_mode(orig_mode); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Undo::execute(RunContext& context) +{ +} + +void +Undo::post_process() +{ + respond(); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Undo.hpp b/src/server/events/Undo.hpp new file mode 100644 index 00000000..af4b0d65 --- /dev/null +++ b/src/server/events/Undo.hpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_UNDO_HPP +#define INGEN_EVENTS_UNDO_HPP + +#include "Event.hpp" +#include "UndoStack.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +/** A request to undo the last change to the engine. + * + * \ingroup engine + */ +class Undo : public Event +{ +public: + Undo(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Undo& msg); + + Undo(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Redo& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + +private: + UndoStack::Entry _entry; + bool _is_redo; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_UNDO_HPP diff --git a/src/server/ingen_engine.cpp b/src/server/ingen_engine.cpp new file mode 100644 index 00000000..3409f1bf --- /dev/null +++ b/src/server/ingen_engine.cpp @@ -0,0 +1,44 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Module.hpp" +#include "ingen/World.hpp" +#include "Engine.hpp" +#include "EventWriter.hpp" +#include "util.hpp" + +using namespace Ingen; + +struct IngenEngineModule : public Ingen::Module { + virtual void load(Ingen::World* world) { + Server::set_denormal_flags(world->log()); + SPtr<Server::Engine> engine(new Server::Engine(world)); + world->set_engine(engine); + if (!world->interface()) { + world->set_interface(engine->interface()); + } + } +}; + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + return new IngenEngineModule(); +} + +} // extern "C" diff --git a/src/server/ingen_jack.cpp b/src/server/ingen_jack.cpp new file mode 100644 index 00000000..a897f130 --- /dev/null +++ b/src/server/ingen_jack.cpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/Atom.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/World.hpp" + +#include "JackDriver.hpp" +#include "Engine.hpp" + +using namespace Ingen; + +struct IngenJackModule : public Ingen::Module { + void load(Ingen::World* world) { + if (((Server::Engine*)world->engine().get())->driver()) { + world->log().warn("Engine already has a driver\n"); + return; + } + + Server::JackDriver* driver = new Server::JackDriver( + *(Server::Engine*)world->engine().get()); + const Atom& s = world->conf().option("jack-server"); + const std::string server_name = s.is_valid() ? s.ptr<char>() : ""; + driver->attach(server_name, + world->conf().option("jack-name").ptr<char>(), + nullptr); + ((Server::Engine*)world->engine().get())->set_driver( + SPtr<Server::Driver>(driver)); + } +}; + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + return new IngenJackModule(); +} + +} // extern "C" diff --git a/src/server/ingen_lv2.cpp b/src/server/ingen_lv2.cpp new file mode 100644 index 00000000..b2806ab6 --- /dev/null +++ b/src/server/ingen_lv2.cpp @@ -0,0 +1,850 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <string> +#include <thread> +#include <vector> + +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/log/logger.h" +#include "lv2/lv2plug.in/ns/ext/options/options.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +#include "ingen/AtomReader.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Parser.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/Store.hpp" +#include "ingen/URI.hpp" +#include "ingen/World.hpp" +#include "ingen/ingen.h" +#include "ingen/runtime_paths.hpp" +#include "ingen/types.hpp" +#include "raul/Semaphore.hpp" + +#include "Buffer.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "EnginePort.hpp" +#include "EventWriter.hpp" +#include "GraphImpl.hpp" +#include "PostProcessor.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" + +#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define NS_RDFS "http://www.w3.org/2000/01/rdf-schema#" + +namespace Ingen { + +/** Record of a graph in this bundle. */ +struct LV2Graph : public Parser::ResourceRecord { + LV2Graph(Parser::ResourceRecord record); + + LV2_Descriptor descriptor; +}; + +/** Ingen LV2 library. */ +class Lib { +public: + explicit Lib(const char* bundle_path); + + typedef std::vector< SPtr<const LV2Graph> > Graphs; + + Graphs graphs; +}; + +namespace Server { + +class LV2Driver; + +void signal_main(RunContext& context, LV2Driver* driver); + +inline size_t +ui_ring_size(SampleCount block_length) +{ + return std::max((size_t)8192, (size_t)block_length * 16); +} + +class LV2Driver : public Ingen::Server::Driver + , public Ingen::AtomSink +{ +public: + LV2Driver(Engine& engine, + SampleCount block_length, + size_t seq_size, + SampleCount sample_rate) + : _engine(engine) + , _main_sem(0) + , _reader(engine.world()->uri_map(), + engine.world()->uris(), + engine.world()->log(), + *engine.world()->interface().get()) + , _writer(engine.world()->uri_map(), + engine.world()->uris(), + *this) + , _from_ui(ui_ring_size(block_length)) + , _to_ui(ui_ring_size(block_length)) + , _root_graph(nullptr) + , _notify_capacity(0) + , _block_length(block_length) + , _seq_size(seq_size) + , _sample_rate(sample_rate) + , _frame_time(0) + , _to_ui_overflow_sem(0) + , _to_ui_overflow(false) + , _instantiated(false) + {} + + virtual bool dynamic_ports() const { return !_instantiated; } + + void pre_process_port(RunContext& context, EnginePort* port) { + const URIs& uris = _engine.world()->uris(); + const SampleCount nframes = context.nframes(); + DuplexPort* graph_port = port->graph_port(); + Buffer* graph_buf = graph_port->buffer(0).get(); + void* lv2_buf = port->buffer(); + + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + graph_port->set_driver_buffer(lv2_buf, nframes * sizeof(float)); + } else if (graph_port->buffer_type() == uris.atom_Sequence) { + graph_port->set_driver_buffer(lv2_buf, lv2_atom_total_size((LV2_Atom*)lv2_buf)); + if (graph_port->symbol() == "control") { // TODO: Safe to use index? + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)lv2_buf; + bool enqueued = false; + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (AtomReader::is_message(uris, &ev->body)) { + enqueued = enqueue_message(&ev->body) || enqueued; + } + } + + if (enqueued) { + // Enqueued a message for processing, raise semaphore + _main_sem.post(); + } + } + } + + if (graph_port->is_input()) { + graph_port->monitor(context); + } else { + graph_buf->prepare_write(context); + } + } + + void post_process_port(RunContext& context, EnginePort* port) { + DuplexPort* graph_port = port->graph_port(); + + // No copying necessary, host buffers are used directly + // Reset graph port buffer pointer to no longer point to the Jack buffer + if (graph_port->is_driver_port()) { + graph_port->set_driver_buffer(nullptr, 0); + } + } + + void run(uint32_t nframes) { + _engine.locate(_frame_time, nframes); + + // Notify buffer is a Chunk with size set to the available space + _notify_capacity = ((LV2_Atom_Sequence*)_ports[1]->buffer())->atom.size; + + for (auto& p : _ports) { + pre_process_port(_engine.run_context(), p); + } + + _engine.run(nframes); + if (_engine.post_processor()->pending()) { + _main_sem.post(); + } + + flush_to_ui(_engine.run_context()); + + for (auto& p : _ports) { + post_process_port(_engine.run_context(), p); + } + + _frame_time += nframes; + } + + virtual void deactivate() { + _engine.quit(); + _main_sem.post(); + } + + virtual void set_root_graph(GraphImpl* graph) { _root_graph = graph; } + virtual GraphImpl* root_graph() { return _root_graph; } + + virtual EnginePort* get_port(const Raul::Path& path) { + for (auto& p : _ports) { + if (p->graph_port()->path() == path) { + return p; + } + } + + return nullptr; + } + + /** Add a port. Called only during init or restore. */ + virtual void add_port(RunContext& context, EnginePort* port) { + const uint32_t index = port->graph_port()->index(); + if (_ports.size() <= index) { + _ports.resize(index + 1); + } + _ports[index] = port; + } + + /** Remove a port. Called only during init or restore. */ + virtual void remove_port(RunContext& context, EnginePort* port) { + const uint32_t index = port->graph_port()->index(); + _ports[index] = nullptr; + } + + /** Unused since LV2 has no dynamic ports. */ + virtual void register_port(EnginePort& port) {} + + /** Unused since LV2 has no dynamic ports. */ + virtual void unregister_port(EnginePort& port) {} + + /** Unused since LV2 has no dynamic ports. */ + virtual void rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) {} + + /** Unused since LV2 has no dynamic ports. */ + virtual void port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) {} + + virtual EnginePort* create_port(DuplexPort* graph_port) { + graph_port->set_is_driver_port(*_engine.buffer_factory()); + return new EnginePort(graph_port); + } + + virtual void append_time_events(RunContext& context, + Buffer& buffer) + { + const URIs& uris = _engine.world()->uris(); + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)_ports[0]->buffer(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->body.type == uris.atom_Object) { + const LV2_Atom_Object* obj = (LV2_Atom_Object*)&ev->body; + if (obj->body.otype == uris.time_Position) { + buffer.append_event(ev->time.frames, + ev->body.size, + ev->body.type, + (const uint8_t*)(&ev->body + 1)); + } + } + } + } + + virtual int real_time_priority() { return 60; } + + /** Called in run thread for events received at control input port. */ + bool enqueue_message(const LV2_Atom* atom) { + if (_from_ui.write(lv2_atom_total_size(atom), atom) == 0) { +#ifndef NDEBUG + _engine.log().error("Control input buffer overflow\n"); +#endif + return false; + } + return true; + } + + Raul::Semaphore& main_sem() { return _main_sem; } + + /** AtomSink::write implementation called by the PostProcessor in the main + * thread to write responses to the UI. + */ + bool write(const LV2_Atom* atom, int32_t default_id) { + // Called from post-processor in main thread + while (_to_ui.write(lv2_atom_total_size(atom), atom) == 0) { + // Overflow, wait until ring is drained next cycle + _to_ui_overflow = true; + _to_ui_overflow_sem.wait(); + _to_ui_overflow = false; + } + return true; + } + + void consume_from_ui() { + const uint32_t read_space = _from_ui.read_space(); + void* buf = nullptr; + for (uint32_t read = 0; read < read_space;) { + LV2_Atom atom; + if (!_from_ui.read(sizeof(LV2_Atom), &atom)) { + _engine.log().rt_error("Error reading head from from-UI ring\n"); + break; + } + + buf = realloc(buf, sizeof(LV2_Atom) + atom.size); + memcpy(buf, &atom, sizeof(LV2_Atom)); + + if (!_from_ui.read(atom.size, (char*)buf + sizeof(LV2_Atom))) { + _engine.log().rt_error("Error reading body from from-UI ring\n"); + break; + } + + _reader.write((LV2_Atom*)buf); + read += sizeof(LV2_Atom) + atom.size; + } + free(buf); + } + + void flush_to_ui(RunContext& context) { + if (_ports.size() < 2) { + _engine.log().rt_error("Standard control ports are not present\n"); + return; + } + + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)_ports[1]->buffer(); + if (!seq) { + _engine.log().rt_error("Notify output not connected\n"); + return; + } + + // Initialise output port buffer to an empty Sequence + seq->atom.type = _engine.world()->uris().atom_Sequence; + seq->atom.size = sizeof(LV2_Atom_Sequence_Body); + + const uint32_t read_space = _to_ui.read_space(); + for (uint32_t read = 0; read < read_space;) { + LV2_Atom atom; + if (!_to_ui.peek(sizeof(LV2_Atom), &atom)) { + _engine.log().rt_error("Error reading head from to-UI ring\n"); + break; + } + + if (seq->atom.size + lv2_atom_pad_size( + sizeof(LV2_Atom_Event) + atom.size) + > _notify_capacity) { + break; // Output port buffer full, resume next time + } + + LV2_Atom_Event* ev = (LV2_Atom_Event*)( + (uint8_t*)seq + lv2_atom_total_size(&seq->atom)); + + ev->time.frames = 0; // TODO: Time? + ev->body = atom; + + _to_ui.skip(sizeof(LV2_Atom)); + if (!_to_ui.read(ev->body.size, LV2_ATOM_BODY(&ev->body))) { + _engine.log().rt_error("Error reading body from to-UI ring\n"); + break; + } + + read += lv2_atom_total_size(&ev->body); + seq->atom.size += lv2_atom_pad_size( + sizeof(LV2_Atom_Event) + ev->body.size); + } + + if (_to_ui_overflow) { + _to_ui_overflow_sem.post(); + } + } + + virtual SampleCount block_length() const { return _block_length; } + virtual size_t seq_size() const { return _seq_size; } + virtual SampleCount sample_rate() const { return _sample_rate; } + virtual SampleCount frame_time() const { return _frame_time; } + + AtomReader& reader() { return _reader; } + AtomWriter& writer() { return _writer; } + + typedef std::vector<EnginePort*> Ports; + + Ports& ports() { return _ports; } + + void set_instantiated(bool instantiated) { _instantiated = instantiated; } + +private: + Engine& _engine; + Ports _ports; + Raul::Semaphore _main_sem; + AtomReader _reader; + AtomWriter _writer; + Raul::RingBuffer _from_ui; + Raul::RingBuffer _to_ui; + GraphImpl* _root_graph; + uint32_t _notify_capacity; + SampleCount _block_length; + size_t _seq_size; + SampleCount _sample_rate; + SampleCount _frame_time; + Raul::Semaphore _to_ui_overflow_sem; + bool _to_ui_overflow; + bool _instantiated; +}; + +} // namespace Server +} // namespace Ingen + +extern "C" { + +using namespace Ingen; +using namespace Ingen::Server; + +static void +ingen_lv2_main(SPtr<Engine> engine, const SPtr<LV2Driver>& driver) +{ + while (true) { + // Wait until there is work to be done + driver->main_sem().wait(); + + // Convert pending messages to events and push to pre processor + driver->consume_from_ui(); + + // Run post processor and maid to finalise events from last time + if (!engine->main_iteration()) { + return; + } + } +} + +struct IngenPlugin { + IngenPlugin() + : world(nullptr) + , main(nullptr) + , map(nullptr) + , argc(0) + , argv(nullptr) + {} + + Ingen::World* world; + SPtr<Engine> engine; + std::thread* main; + LV2_URID_Map* map; + int argc; + char** argv; +}; + +static Lib::Graphs +find_graphs(const URI& manifest_uri) +{ + Sord::World world; + Parser parser; + + const std::set<Parser::ResourceRecord> resources = parser.find_resources( + world, + manifest_uri, + URI(INGEN__Graph)); + + Lib::Graphs graphs; + for (const auto& r : resources) { + graphs.push_back(SPtr<const LV2Graph>(new LV2Graph(r))); + } + + return graphs; +} + +static LV2_Handle +ingen_instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* bundle_path, + const LV2_Feature*const* features) +{ + // Get features from features array + LV2_URID_Map* map = nullptr; + LV2_URID_Unmap* unmap = nullptr; + LV2_Log_Log* log = nullptr; + const LV2_Options_Option* options = nullptr; + for (int i = 0; features[i]; ++i) { + if (!strcmp(features[i]->URI, LV2_URID__map)) { + map = (LV2_URID_Map*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_URID__unmap)) { + unmap = (LV2_URID_Unmap*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_LOG__log)) { + log = (LV2_Log_Log*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_OPTIONS__options)) { + options = (const LV2_Options_Option*)features[i]->data; + } + } + + LV2_Log_Logger logger; + lv2_log_logger_init(&logger, map, log); + + if (!map) { + lv2_log_error(&logger, "host did not provide URI map feature\n"); + return nullptr; + } else if (!unmap) { + lv2_log_error(&logger, "host did not provide URI unmap feature\n"); + return nullptr; + } + + set_bundle_path(bundle_path); + const std::string manifest_path = Ingen::bundle_file_path("manifest.ttl"); + SerdNode manifest_node = serd_node_new_file_uri( + (const uint8_t*)manifest_path.c_str(), nullptr, nullptr, true); + + Lib::Graphs graphs = find_graphs(URI((const char*)manifest_node.buf)); + serd_node_free(&manifest_node); + + const LV2Graph* graph = nullptr; + for (const auto& g : graphs) { + if (g->uri == descriptor->URI) { + graph = g.get(); + break; + } + } + + if (!graph) { + lv2_log_error(&logger, "could not find graph <%s>\n", descriptor->URI); + return nullptr; + } + + IngenPlugin* plugin = new IngenPlugin(); + plugin->map = map; + plugin->world = new Ingen::World(map, unmap, log); + plugin->world->load_configuration(plugin->argc, plugin->argv); + + LV2_URID bufsz_max = map->map(map->handle, LV2_BUF_SIZE__maxBlockLength); + LV2_URID bufsz_seq = map->map(map->handle, LV2_BUF_SIZE__sequenceSize); + LV2_URID atom_Int = map->map(map->handle, LV2_ATOM__Int); + int32_t block_length = 0; + int32_t seq_size = 0; + if (options) { + for (const LV2_Options_Option* o = options; o->key; ++o) { + if (o->key == bufsz_max && o->type == atom_Int) { + block_length = *(const int32_t*)o->value; + } else if (o->key == bufsz_seq && o->type == atom_Int) { + seq_size = *(const int32_t*)o->value; + } + } + } + if (block_length == 0) { + block_length = 4096; + plugin->world->log().warn("No maximum block length given\n"); + } + if (seq_size == 0) { + seq_size = 16384; + plugin->world->log().warn("No maximum sequence size given\n"); + } + + plugin->world->log().info( + fmt("Block: %1% frames, Sequence: %2% bytes\n") + % block_length % seq_size); + plugin->world->conf().set( + "queue-size", + plugin->world->forge().make(std::max(block_length, seq_size) * 4)); + + SPtr<Server::Engine> engine(new Server::Engine(plugin->world)); + plugin->engine = engine; + plugin->world->set_engine(engine); + + SPtr<Interface> interface = engine->interface(); + + plugin->world->set_interface(interface); + + Server::ThreadManager::set_flag(Server::THREAD_PRE_PROCESS); + Server::ThreadManager::single_threaded = true; + + LV2Driver* driver = new LV2Driver(*engine.get(), block_length, seq_size, rate); + engine->set_driver(SPtr<Ingen::Server::Driver>(driver)); + + engine->activate(); + Server::ThreadManager::single_threaded = true; + + std::lock_guard<std::mutex> lock(plugin->world->rdf_mutex()); + + // Locate to time 0 to process initialization events + engine->locate(0, block_length); + engine->post_processor()->set_end_time(block_length); + + // Parse graph, filling the queue with events to create it + plugin->world->interface()->bundle_begin(); + plugin->world->parser()->parse_file(plugin->world, + plugin->world->interface().get(), + graph->filename); + plugin->world->interface()->bundle_end(); + + // Drain event queue + while (engine->pending_events()) { + engine->process_all_events(); + engine->post_processor()->process(); + engine->maid()->cleanup(); + } + + /* Register client after loading graph so the to-ui ring does not overflow. + Since we are not yet rolling, it won't be drained, causing a deadlock. */ + SPtr<Interface> client(&driver->writer(), NullDeleter<Interface>); + interface->set_respondee(client); + engine->register_client(client); + + driver->set_instantiated(true); + return (LV2_Handle)plugin; +} + +static void +ingen_connect_port(LV2_Handle instance, uint32_t port, void* data) +{ + using namespace Ingen::Server; + + IngenPlugin* me = (IngenPlugin*)instance; + Server::Engine* engine = (Server::Engine*)me->world->engine().get(); + const SPtr<LV2Driver>& driver = static_ptr_cast<LV2Driver>(engine->driver()); + if (port < driver->ports().size()) { + driver->ports().at(port)->set_buffer(data); + } else { + engine->log().rt_error("Connect to non-existent port\n"); + } +} + +static void +ingen_activate(LV2_Handle instance) +{ + IngenPlugin* me = (IngenPlugin*)instance; + SPtr<Server::Engine> engine = static_ptr_cast<Server::Engine>(me->world->engine()); + const SPtr<LV2Driver>& driver = static_ptr_cast<LV2Driver>(engine->driver()); + engine->activate(); + me->main = new std::thread(ingen_lv2_main, engine, driver); +} + +static void +ingen_run(LV2_Handle instance, uint32_t sample_count) +{ + IngenPlugin* me = (IngenPlugin*)instance; + SPtr<Server::Engine> engine = static_ptr_cast<Server::Engine>(me->world->engine()); + const SPtr<LV2Driver>& driver = static_ptr_cast<LV2Driver>(engine->driver()); + + Server::ThreadManager::set_flag(Ingen::Server::THREAD_PROCESS); + Server::ThreadManager::set_flag(Ingen::Server::THREAD_IS_REAL_TIME); + + driver->run(sample_count); +} + +static void +ingen_deactivate(LV2_Handle instance) +{ + IngenPlugin* me = (IngenPlugin*)instance; + me->world->engine()->deactivate(); + if (me->main) { + me->main->join(); + delete me->main; + me->main = nullptr; + } +} + +static void +ingen_cleanup(LV2_Handle instance) +{ + IngenPlugin* me = (IngenPlugin*)instance; + me->world->set_engine(SPtr<Ingen::Server::Engine>()); + me->world->set_interface(SPtr<Ingen::Interface>()); + if (me->main) { + me->main->join(); + delete me->main; + } + + World* world = me->world; + delete me; + delete world; +} + +static void +get_state_features(const LV2_Feature* const* features, + LV2_State_Map_Path** map, + LV2_State_Make_Path** make) +{ + for (int i = 0; features[i]; ++i) { + if (map && !strcmp(features[i]->URI, LV2_STATE__mapPath)) { + *map = (LV2_State_Map_Path*)features[i]->data; + } else if (make && !strcmp(features[i]->URI, LV2_STATE__makePath)) { + *make = (LV2_State_Make_Path*)features[i]->data; + } + } +} + +static LV2_State_Status +ingen_save(LV2_Handle instance, + LV2_State_Store_Function store, + LV2_State_Handle handle, + uint32_t flags, + const LV2_Feature* const* features) +{ + IngenPlugin* plugin = (IngenPlugin*)instance; + + LV2_State_Map_Path* map_path = nullptr; + LV2_State_Make_Path* make_path = nullptr; + get_state_features(features, &map_path, &make_path); + if (!map_path || !make_path || !plugin->map) { + plugin->world->log().error("Missing state:mapPath, state:makePath, or urid:Map\n"); + return LV2_STATE_ERR_NO_FEATURE; + } + + LV2_URID ingen_file = plugin->map->map(plugin->map->handle, INGEN__file); + LV2_URID atom_Path = plugin->map->map(plugin->map->handle, + LV2_ATOM__Path); + + char* real_path = make_path->path(make_path->handle, "main.ttl"); + char* state_path = map_path->abstract_path(map_path->handle, real_path); + + auto root = plugin->world->store()->find(Raul::Path("/")); + + { + std::lock_guard<std::mutex> lock(plugin->world->rdf_mutex()); + + plugin->world->serialiser()->start_to_file(root->second->path(), real_path); + plugin->world->serialiser()->serialise(root->second); + plugin->world->serialiser()->finish(); + } + + store(handle, + ingen_file, + state_path, + strlen(state_path) + 1, + atom_Path, + LV2_STATE_IS_POD); + + free(state_path); + free(real_path); + return LV2_STATE_SUCCESS; +} + +static LV2_State_Status +ingen_restore(LV2_Handle instance, + LV2_State_Retrieve_Function retrieve, + LV2_State_Handle handle, + uint32_t flags, + const LV2_Feature* const* features) +{ + IngenPlugin* plugin = (IngenPlugin*)instance; + + LV2_State_Map_Path* map_path = nullptr; + get_state_features(features, &map_path, nullptr); + if (!map_path) { + plugin->world->log().error("Missing state:mapPath\n"); + return LV2_STATE_ERR_NO_FEATURE; + } + + LV2_URID ingen_file = plugin->map->map(plugin->map->handle, INGEN__file); + size_t size; + uint32_t type; + uint32_t valflags; + + // Get abstract path to graph file + const char* path = (const char*)retrieve( + handle, ingen_file, &size, &type, &valflags); + if (!path) { + return LV2_STATE_ERR_NO_PROPERTY; + } + + // Convert to absolute path + char* real_path = map_path->absolute_path(map_path->handle, path); + if (!real_path) { + return LV2_STATE_ERR_UNKNOWN; + } + +#if 0 + // Remove existing root graph contents + SPtr<Engine> engine = plugin->engine; + for (const auto& b : engine->root_graph()->blocks()) { + plugin->world->interface()->del(b.uri()); + } + + const uint32_t n_ports = engine->root_graph()->num_ports_non_rt(); + for (int32_t i = n_ports - 1; i >= 0; --i) { + PortImpl* port = engine->root_graph()->port_impl(i); + if (port->symbol() != "control" && port->symbol() != "notify") { + plugin->world->interface()->del(port->uri()); + } + } +#endif + + // Load new graph + std::lock_guard<std::mutex> lock(plugin->world->rdf_mutex()); + plugin->world->parser()->parse_file( + plugin->world, plugin->world->interface().get(), real_path); + + free(real_path); + return LV2_STATE_SUCCESS; +} + +static const void* +ingen_extension_data(const char* uri) +{ + static const LV2_State_Interface state = { ingen_save, ingen_restore }; + if (!strcmp(uri, LV2_STATE__interface)) { + return &state; + } + return nullptr; +} + +LV2Graph::LV2Graph(Parser::ResourceRecord record) + : Parser::ResourceRecord(std::move(record)) +{ + descriptor.URI = uri.c_str(); + descriptor.instantiate = ingen_instantiate; + descriptor.connect_port = ingen_connect_port; + descriptor.activate = ingen_activate; + descriptor.run = ingen_run; + descriptor.deactivate = ingen_deactivate; + descriptor.cleanup = ingen_cleanup; + descriptor.extension_data = ingen_extension_data; +} + +Lib::Lib(const char* bundle_path) +{ + Ingen::set_bundle_path(bundle_path); + const std::string manifest_path = Ingen::bundle_file_path("manifest.ttl"); + SerdNode manifest_node = serd_node_new_file_uri( + (const uint8_t*)manifest_path.c_str(), nullptr, nullptr, true); + + graphs = find_graphs(URI((const char*)manifest_node.buf)); + + serd_node_free(&manifest_node); +} + +static void +lib_cleanup(LV2_Lib_Handle handle) +{ + Lib* lib = (Lib*)handle; + delete lib; +} + +static const LV2_Descriptor* +lib_get_plugin(LV2_Lib_Handle handle, uint32_t index) +{ + Lib* lib = (Lib*)handle; + return index < lib->graphs.size() ? &lib->graphs[index]->descriptor : nullptr; +} + +/** LV2 plugin library entry point */ +LV2_SYMBOL_EXPORT +const LV2_Lib_Descriptor* +lv2_lib_descriptor(const char* bundle_path, + const LV2_Feature*const* features) +{ + static const uint32_t desc_size = sizeof(LV2_Lib_Descriptor); + Lib* lib = new Lib(bundle_path); + + // FIXME: memory leak. I think the LV2_Lib_Descriptor API is botched :( + LV2_Lib_Descriptor* desc = (LV2_Lib_Descriptor*)malloc(desc_size); + desc->handle = lib; + desc->size = desc_size; + desc->cleanup = lib_cleanup; + desc->get_plugin = lib_get_plugin; + + return desc; +} + +} // extern "C" diff --git a/src/server/ingen_portaudio.cpp b/src/server/ingen_portaudio.cpp new file mode 100644 index 00000000..e4065342 --- /dev/null +++ b/src/server/ingen_portaudio.cpp @@ -0,0 +1,54 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/Atom.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/World.hpp" + +#include "PortAudioDriver.hpp" +#include "Engine.hpp" + +using namespace Ingen; + +struct IngenPortAudioModule : public Ingen::Module { + void load(Ingen::World* world) { + if (((Server::Engine*)world->engine().get())->driver()) { + world->log().warn("Engine already has a driver\n"); + return; + } + + Server::PortAudioDriver* driver = new Server::PortAudioDriver( + *(Server::Engine*)world->engine().get()); + driver->attach(); + ((Server::Engine*)world->engine().get())->set_driver( + SPtr<Server::Driver>(driver)); + } +}; + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + return new IngenPortAudioModule(); +} + +} // extern "C" diff --git a/src/server/internals/BlockDelay.cpp b/src/server/internals/BlockDelay.cpp new file mode 100644 index 00000000..6b27ed83 --- /dev/null +++ b/src/server/internals/BlockDelay.cpp @@ -0,0 +1,89 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <climits> + +#include <cmath> + +#include "ingen/URIs.hpp" +#include "raul/Array.hpp" +#include "raul/Maid.hpp" + +#include "Buffer.hpp" +#include "InputPort.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "RunContext.hpp" +#include "internals/BlockDelay.hpp" + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* BlockDelayNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "BlockDelay"), Raul::Symbol("blockDelay")); +} + +BlockDelayNode::BlockDelayNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, polyphonic, parent, srate) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(2); + + _in_port = new InputPort(bufs, this, Raul::Symbol("in"), 0, 1, + PortType::AUDIO, 0, bufs.forge().make(0.0f)); + _in_port->set_property(uris.lv2_name, bufs.forge().alloc("In")); + _ports->at(0) = _in_port; + + _out_port = new OutputPort(bufs, this, Raul::Symbol("out"), 0, 1, + PortType::AUDIO, 0, bufs.forge().make(0.0f)); + _out_port->set_property(uris.lv2_name, bufs.forge().alloc("Out")); + _ports->at(1) = _out_port; +} + +BlockDelayNode::~BlockDelayNode() +{ + _buffer.reset(); +} + +void +BlockDelayNode::activate(BufferFactory& bufs) +{ + _buffer = bufs.create( + bufs.uris().atom_Sound, 0, bufs.audio_buffer_size()); + + BlockImpl::activate(bufs); +} + +void +BlockDelayNode::run(RunContext& context) +{ + // Copy buffer from last cycle to output + _out_port->buffer(0)->copy(context, _buffer.get()); + + // Copy input from this cycle to buffer + _buffer->copy(context, _in_port->buffer(0).get()); +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/BlockDelay.hpp b/src/server/internals/BlockDelay.hpp new file mode 100644 index 00000000..e1ef5311 --- /dev/null +++ b/src/server/internals/BlockDelay.hpp @@ -0,0 +1,62 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_BLOCKDELAY_HPP +#define INGEN_INTERNALS_BLOCKDELAY_HPP + +#include "BufferRef.hpp" +#include "InternalBlock.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; +class BufferFactory; + +namespace Internals { + +class BlockDelayNode : public InternalBlock +{ +public: + BlockDelayNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + ~BlockDelayNode(); + + void activate(BufferFactory& bufs); + + void run(RunContext& context); + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + InputPort* _in_port; + OutputPort* _out_port; + BufferRef _buffer; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_BLOCKDELAY_HPP diff --git a/src/server/internals/Controller.cpp b/src/server/internals/Controller.cpp new file mode 100644 index 00000000..4c1cf45a --- /dev/null +++ b/src/server/internals/Controller.cpp @@ -0,0 +1,174 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/URIs.hpp" +#include "internals/Controller.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "InputPort.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "PostProcessor.hpp" +#include "RunContext.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* ControllerNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "Controller"), Raul::Symbol("controller")); +} + +ControllerNode::ControllerNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, false, parent, srate) + , _learning(false) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(7); + + const Atom zero = bufs.forge().make(0.0f); + const Atom one = bufs.forge().make(1.0f); + const Atom atom_Float = bufs.forge().make_urid(URI(LV2_ATOM__Float)); + + _midi_in_port = new InputPort(bufs, this, Raul::Symbol("input"), 0, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_in_port->set_property(uris.lv2_name, bufs.forge().alloc("Input")); + _midi_in_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(0) = _midi_in_port; + + _midi_out_port = new OutputPort(bufs, this, Raul::Symbol("event"), 1, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_out_port->set_property(uris.lv2_name, bufs.forge().alloc("Event")); + _midi_out_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(1) = _midi_out_port; + + _param_port = new InputPort(bufs, this, Raul::Symbol("controller"), 2, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _param_port->set_property(uris.atom_supports, atom_Float); + _param_port->set_property(uris.lv2_minimum, zero); + _param_port->set_property(uris.lv2_maximum, bufs.forge().make(127.0f)); + _param_port->set_property(uris.lv2_portProperty, uris.lv2_integer); + _param_port->set_property(uris.lv2_name, bufs.forge().alloc("Controller")); + _ports->at(2) = _param_port; + + _log_port = new InputPort(bufs, this, Raul::Symbol("logarithmic"), 3, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _log_port->set_property(uris.atom_supports, atom_Float); + _log_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _log_port->set_property(uris.lv2_name, bufs.forge().alloc("Logarithmic")); + _ports->at(3) = _log_port; + + _min_port = new InputPort(bufs, this, Raul::Symbol("minimum"), 4, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _min_port->set_property(uris.atom_supports, atom_Float); + _min_port->set_property(uris.lv2_name, bufs.forge().alloc("Minimum")); + _ports->at(4) = _min_port; + + _max_port = new InputPort(bufs, this, Raul::Symbol("maximum"), 5, 1, + PortType::ATOM, uris.atom_Sequence, one); + _max_port->set_property(uris.atom_supports, atom_Float); + _max_port->set_property(uris.lv2_name, bufs.forge().alloc("Maximum")); + _ports->at(5) = _max_port; + + _audio_port = new OutputPort(bufs, this, Raul::Symbol("output"), 6, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _audio_port->set_property(uris.atom_supports, atom_Float); + _audio_port->set_property(uris.lv2_name, bufs.forge().alloc("Output")); + _ports->at(6) = _audio_port; +} + +void +ControllerNode::run(RunContext& context) +{ + const BufferRef midi_in = _midi_in_port->buffer(0); + LV2_Atom_Sequence* seq = midi_in->get<LV2_Atom_Sequence>(); + const BufferRef midi_out = _midi_out_port->buffer(0); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY(&ev->body); + if (ev->body.type == _midi_in_port->bufs().uris().midi_MidiEvent && + ev->body.size >= 3 && + lv2_midi_message_type(buf) == LV2_MIDI_MSG_CONTROLLER) { + if (control(context, buf[1], buf[2], ev->time.frames + context.start())) { + midi_out->append_event(ev->time.frames, &ev->body); + } + } + } +} + +bool +ControllerNode::control(RunContext& context, uint8_t control_num, uint8_t val, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + const uint32_t offset = time - context.start(); + + const Sample nval = (val / 127.0f); // normalized [0, 1] + + if (_learning) { + _param_port->set_control_value(context, time, control_num); + _param_port->force_monitor_update(); + _learning = false; + } else { + _param_port->update_values(offset, 0); + } + + if (control_num != _param_port->buffer(0)->value_at(offset)) { + return false; + } + + for (const auto& port : { _min_port, _max_port, _log_port }) { + port->update_values(offset, 0); + } + + const Sample min_port_val = _min_port->buffer(0)->value_at(offset); + const Sample max_port_val = _max_port->buffer(0)->value_at(offset); + const Sample log_port_val = _log_port->buffer(0)->value_at(offset); + + Sample scaled_value; + if (log_port_val > 0.0f) { + // haaaaack, stupid negatives and logarithms + Sample log_offset = 0; + if (min_port_val < 0) { + log_offset = fabs(min_port_val); + } + const Sample min = log(min_port_val + 1 + log_offset); + const Sample max = log(max_port_val + 1 + log_offset); + scaled_value = expf(nval * (max - min) + min) - 1 - log_offset; + } else { + scaled_value = ((nval) * (max_port_val - min_port_val)) + min_port_val; + } + + _audio_port->set_control_value(context, time, scaled_value); + + return true; +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/Controller.hpp b/src/server/internals/Controller.hpp new file mode 100644 index 00000000..720f78c0 --- /dev/null +++ b/src/server/internals/Controller.hpp @@ -0,0 +1,71 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_CONTROLLER_HPP +#define INGEN_INTERNALS_CONTROLLER_HPP + +#include "InternalBlock.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; + +namespace Internals { + +/** MIDI control input block. + * + * Creating one of these nodes is how a user makes "MIDI Bindings". Note that + * this node will always be monophonic, the poly parameter is ignored. + * + * \ingroup engine + */ +class ControllerNode : public InternalBlock +{ +public: + ControllerNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + void run(RunContext& context); + + bool control(RunContext& context, uint8_t control_num, uint8_t val, FrameTime time); + + void learn() { _learning = true; } + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + InputPort* _midi_in_port; + OutputPort* _midi_out_port; + InputPort* _param_port; + InputPort* _log_port; + InputPort* _min_port; + InputPort* _max_port; + OutputPort* _audio_port; + bool _learning; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_CONTROLLER_HPP diff --git a/src/server/internals/Note.cpp b/src/server/internals/Note.cpp new file mode 100644 index 00000000..b39dd1d4 --- /dev/null +++ b/src/server/internals/Note.cpp @@ -0,0 +1,420 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "raul/Array.hpp" +#include "raul/Maid.hpp" + +#include "Buffer.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "RunContext.hpp" +#include "ingen_config.h" +#include "internals/Note.hpp" +#include "util.hpp" + +// #define NOTE_DEBUG 1 + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* NoteNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "Note"), Raul::Symbol("note")); +} + +NoteNode::NoteNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, polyphonic, parent, srate) + , _voices(bufs.maid().make_managed<Voices>(_polyphony)) + , _sustain(false) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(8); + + const Atom zero = bufs.forge().make(0.0f); + const Atom one = bufs.forge().make(1.0f); + + _midi_in_port = new InputPort(bufs, this, Raul::Symbol("input"), 0, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_in_port->set_property(uris.lv2_name, bufs.forge().alloc("Input")); + _midi_in_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(0) = _midi_in_port; + + _freq_port = new OutputPort(bufs, this, Raul::Symbol("frequency"), 1, _polyphony, + PortType::ATOM, uris.atom_Sequence, + bufs.forge().make(440.0f)); + _freq_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _freq_port->set_property(uris.lv2_name, bufs.forge().alloc("Frequency")); + _freq_port->set_property(uris.lv2_minimum, bufs.forge().make(16.0f)); + _freq_port->set_property(uris.lv2_maximum, bufs.forge().make(25088.0f)); + _ports->at(1) = _freq_port; + + _num_port = new OutputPort(bufs, this, Raul::Symbol("number"), 1, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _num_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _num_port->set_property(uris.lv2_minimum, zero); + _num_port->set_property(uris.lv2_maximum, bufs.forge().make(127.0f)); + _num_port->set_property(uris.lv2_portProperty, uris.lv2_integer); + _num_port->set_property(uris.lv2_name, bufs.forge().alloc("Number")); + _ports->at(2) = _num_port; + + _vel_port = new OutputPort(bufs, this, Raul::Symbol("velocity"), 2, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _vel_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _vel_port->set_property(uris.lv2_minimum, zero); + _vel_port->set_property(uris.lv2_maximum, one); + _vel_port->set_property(uris.lv2_name, bufs.forge().alloc("Velocity")); + _ports->at(3) = _vel_port; + + _gate_port = new OutputPort(bufs, this, Raul::Symbol("gate"), 3, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _gate_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _gate_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _gate_port->set_property(uris.lv2_name, bufs.forge().alloc("Gate")); + _ports->at(4) = _gate_port; + + _trig_port = new OutputPort(bufs, this, Raul::Symbol("trigger"), 4, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _trig_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _trig_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _trig_port->set_property(uris.lv2_name, bufs.forge().alloc("Trigger")); + _ports->at(5) = _trig_port; + + _bend_port = new OutputPort(bufs, this, Raul::Symbol("bend"), 5, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _bend_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _bend_port->set_property(uris.lv2_name, bufs.forge().alloc("Bender")); + _bend_port->set_property(uris.lv2_default, zero); + _bend_port->set_property(uris.lv2_minimum, bufs.forge().make(-1.0f)); + _bend_port->set_property(uris.lv2_maximum, one); + _ports->at(6) = _bend_port; + + _pressure_port = new OutputPort(bufs, this, Raul::Symbol("pressure"), 6, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _pressure_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _pressure_port->set_property(uris.lv2_name, bufs.forge().alloc("Pressure")); + _pressure_port->set_property(uris.lv2_default, zero); + _pressure_port->set_property(uris.lv2_minimum, zero); + _pressure_port->set_property(uris.lv2_maximum, one); + _ports->at(7) = _pressure_port; +} + +bool +NoteNode::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + if (!_polyphonic) { + return true; + } + + BlockImpl::prepare_poly(bufs, poly); + + if (_prepared_voices && poly <= _prepared_voices->size()) { + return true; + } + + _prepared_voices = bufs.maid().make_managed<Voices>( + poly, *_voices, Voice()); + + return true; +} + +bool +NoteNode::apply_poly(RunContext& context, uint32_t poly) +{ + if (!BlockImpl::apply_poly(context, poly)) { + return false; + } + + if (_prepared_voices) { + assert(_polyphony <= _prepared_voices->size()); + _voices = std::move(_prepared_voices); + } + assert(_polyphony <= _voices->size()); + + return true; +} + +void +NoteNode::run(RunContext& context) +{ + Buffer* const midi_in = _midi_in_port->buffer(0).get(); + LV2_Atom_Sequence* seq = midi_in->get<LV2_Atom_Sequence>(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY_CONST(&ev->body); + const FrameTime time = context.start() + (FrameTime)ev->time.frames; + if (ev->body.type == _midi_in_port->bufs().uris().midi_MidiEvent && + ev->body.size >= 3) { + switch (lv2_midi_message_type(buf)) { + case LV2_MIDI_MSG_NOTE_ON: + if (buf[2] == 0) { + note_off(context, buf[1], time); + } else { + note_on(context, buf[1], buf[2], time); + } + break; + case LV2_MIDI_MSG_NOTE_OFF: + note_off(context, buf[1], time); + break; + case LV2_MIDI_MSG_CONTROLLER: + switch (buf[1]) { + case LV2_MIDI_CTL_ALL_NOTES_OFF: + case LV2_MIDI_CTL_ALL_SOUNDS_OFF: + all_notes_off(context, time); + break; + case LV2_MIDI_CTL_SUSTAIN: + if (buf[2] > 63) { + sustain_on(context, time); + } else { + sustain_off(context, time); + } + break; + } + break; + case LV2_MIDI_MSG_BENDER: + bend(context, time, (((((uint16_t)buf[2] << 7) | buf[1]) - 8192.0f) + / 8192.0f)); + break; + case LV2_MIDI_MSG_CHANNEL_PRESSURE: + channel_pressure(context, time, buf[1] / 127.0f); + break; + case LV2_MIDI_MSG_NOTE_PRESSURE: + note_pressure(context, time, buf[1], buf[2] / 127.0f); + break; + default: + break; + } + } + } +} + +static inline float +note_to_freq(uint8_t num) +{ + static const float A4 = 440.0f; + return A4 * powf(2.0f, (float)(num - 57.0f) / 12.0f); +} + +void +NoteNode::note_on(RunContext& context, uint8_t note_num, uint8_t velocity, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + assert(note_num <= 127); + + Key* key = &_keys[note_num]; + Voice* voice = nullptr; + uint32_t voice_num = 0; + + if (key->state != Key::State::OFF) { + return; + } + + // Look for free voices + for (uint32_t i=0; i < _polyphony; ++i) { + if ((*_voices)[i].state == Voice::State::FREE) { + voice = &(*_voices)[i]; + voice_num = i; + break; + } + } + + // If we didn't find a free one, steal the oldest + if (voice == nullptr) { + voice_num = 0; + voice = &(*_voices)[0]; + FrameTime oldest_time = (*_voices)[0].time; + for (uint32_t i=1; i < _polyphony; ++i) { + if ((*_voices)[i].time < oldest_time) { + voice = &(*_voices)[i]; + voice_num = i; + oldest_time = voice->time; + } + } + } + assert(voice != nullptr); + assert(voice == &(*_voices)[voice_num]); + + // Update stolen key, if applicable + if (voice->state == Voice::State::ACTIVE) { + assert(_keys[voice->note].state == Key::State::ON_ASSIGNED); + assert(_keys[voice->note].voice == voice_num); + _keys[voice->note].state = Key::State::ON_UNASSIGNED; + } + + // Store key information for later reallocation on note off + key->state = Key::State::ON_ASSIGNED; + key->voice = voice_num; + key->time = time; + + // Check if we just triggered this voice at the same time + // (Double note-on at the same sample on the same voice) + const bool double_trigger = (voice->state == Voice::State::ACTIVE && + voice->time == time); + + // Trigger voice + voice->state = Voice::State::ACTIVE; + voice->note = note_num; + voice->time = time; + + assert(_keys[voice->note].state == Key::State::ON_ASSIGNED); + assert(_keys[voice->note].voice == voice_num); + + _freq_port->set_voice_value(context, voice_num, time, note_to_freq(note_num)); + _num_port->set_voice_value(context, voice_num, time, (float)note_num); + _vel_port->set_voice_value(context, voice_num, time, velocity / 127.0f); + _gate_port->set_voice_value(context, voice_num, time, 1.0f); + if (!double_trigger) { + _trig_port->set_voice_value(context, voice_num, time, 1.0f); + _trig_port->set_voice_value(context, voice_num, time + 1, 0.0f); + } + + assert(key->state == Key::State::ON_ASSIGNED); + assert(voice->state == Voice::State::ACTIVE); + assert(key->voice == voice_num); + assert((*_voices)[key->voice].note == note_num); +} + +void +NoteNode::note_off(RunContext& context, uint8_t note_num, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + + Key* key = &_keys[note_num]; + + if (key->state == Key::State::ON_ASSIGNED) { + // Assigned key, turn off voice and key + if ((*_voices)[key->voice].state == Voice::State::ACTIVE) { + assert((*_voices)[key->voice].note == note_num); + if ( ! _sustain) { + free_voice(context, key->voice, time); + } else { + (*_voices)[key->voice].state = Voice::State::HOLDING; + } + } + } + + key->state = Key::State::OFF; +} + +void +NoteNode::free_voice(RunContext& context, uint32_t voice, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + + // Find a key to reassign to the freed voice (the newest, if there is one) + Key* replace_key = nullptr; + uint8_t replace_key_num = 0; + + for (uint8_t i = 0; i <= 127; ++i) { + if (_keys[i].state == Key::State::ON_UNASSIGNED) { + if (replace_key == nullptr || _keys[i].time > replace_key->time) { + replace_key = &_keys[i]; + replace_key_num = i; + } + } + } + + if (replace_key != nullptr) { // Found a key to assign to freed voice + assert(&_keys[replace_key_num] == replace_key); + assert(replace_key->state == Key::State::ON_UNASSIGNED); + + // Change the freq but leave the gate high and don't retrigger + _freq_port->set_voice_value(context, voice, time, note_to_freq(replace_key_num)); + _num_port->set_voice_value(context, voice, time, replace_key_num); + + replace_key->state = Key::State::ON_ASSIGNED; + replace_key->voice = voice; + _keys[(*_voices)[voice].note].state = Key::State::ON_UNASSIGNED; + (*_voices)[voice].note = replace_key_num; + (*_voices)[voice].state = Voice::State::ACTIVE; + } else { + // No new note for voice, deactivate (set gate low) + _gate_port->set_voice_value(context, voice, time, 0.0f); + (*_voices)[voice].state = Voice::State::FREE; + } +} + +void +NoteNode::all_notes_off(RunContext& context, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + + // FIXME: set all keys to Key::OFF? + + for (uint32_t i = 0; i < _polyphony; ++i) { + _gate_port->set_voice_value(context, i, time, 0.0f); + (*_voices)[i].state = Voice::State::FREE; + } +} + +void +NoteNode::sustain_on(RunContext& context, FrameTime time) +{ + _sustain = true; +} + +void +NoteNode::sustain_off(RunContext& context, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + + _sustain = false; + + for (uint32_t i=0; i < _polyphony; ++i) { + if ((*_voices)[i].state == Voice::State::HOLDING) { + free_voice(context, i, time); + } + } +} + +void +NoteNode::bend(RunContext& context, FrameTime time, float amount) +{ + _bend_port->set_control_value(context, time, amount); +} + +void +NoteNode::note_pressure(RunContext& context, FrameTime time, uint8_t note_num, float amount) +{ + for (uint32_t i=0; i < _polyphony; ++i) { + if ((*_voices)[i].state != Voice::State::FREE && (*_voices)[i].note == note_num) { + _pressure_port->set_voice_value(context, i, time, amount); + return; + } + } +} + +void +NoteNode::channel_pressure(RunContext& context, FrameTime time, float amount) +{ + _pressure_port->set_control_value(context, time, amount); +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/Note.hpp b/src/server/internals/Note.hpp new file mode 100644 index 00000000..1e60c130 --- /dev/null +++ b/src/server/internals/Note.hpp @@ -0,0 +1,109 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_NOTE_HPP +#define INGEN_INTERNALS_NOTE_HPP + +#include "InternalBlock.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; + +namespace Internals { + +/** MIDI note input block. + * + * For pitched instruments like keyboard, etc. + * + * \ingroup engine + */ +class NoteNode : public InternalBlock +{ +public: + NoteNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + bool prepare_poly(BufferFactory& bufs, uint32_t poly); + bool apply_poly(RunContext& context, uint32_t poly); + + void run(RunContext& context); + + void note_on(RunContext& context, uint8_t note_num, uint8_t velocity, FrameTime time); + void note_off(RunContext& context, uint8_t note_num, FrameTime time); + void all_notes_off(RunContext& context, FrameTime time); + + void sustain_on(RunContext& context, FrameTime time); + void sustain_off(RunContext& context, FrameTime time); + + void bend(RunContext& context, FrameTime time, float amount); + void note_pressure(RunContext& context, FrameTime time, uint8_t note_num, float amount); + void channel_pressure(RunContext& context, FrameTime time, float amount); + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + /** Key, one for each key on the keyboard */ + struct Key { + enum class State { OFF, ON_ASSIGNED, ON_UNASSIGNED }; + Key() : state(State::OFF), voice(0), time(0) {} + State state; + uint32_t voice; + SampleCount time; + }; + + /** Voice, one of these always exists for each voice */ + struct Voice { + enum class State { FREE, ACTIVE, HOLDING }; + Voice() : state(State::FREE), note(0), time(0) {} + State state; + uint8_t note; + SampleCount time; + }; + + typedef Raul::Array<Voice> Voices; + + void free_voice(RunContext& context, uint32_t voice, FrameTime time); + + MPtr<Voices> _voices; + MPtr<Voices> _prepared_voices; + + Key _keys[128]; + bool _sustain; ///< Whether or not hold pedal is depressed + + InputPort* _midi_in_port; + OutputPort* _freq_port; + OutputPort* _num_port; + OutputPort* _vel_port; + OutputPort* _gate_port; + OutputPort* _trig_port; + OutputPort* _bend_port; + OutputPort* _pressure_port; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_NOTE_HPP diff --git a/src/server/internals/Time.cpp b/src/server/internals/Time.cpp new file mode 100644 index 00000000..5474bf21 --- /dev/null +++ b/src/server/internals/Time.cpp @@ -0,0 +1,78 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "Buffer.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "RunContext.hpp" +#include "internals/Time.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* TimeNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "Time"), Raul::Symbol("time")); +} + +TimeNode::TimeNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, false, parent, srate) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(1); + + _notify_port = new OutputPort( + bufs, this, Raul::Symbol("notify"), 0, 1, + PortType::ATOM, uris.atom_Sequence, Atom(), 1024); + _notify_port->set_property(uris.lv2_name, bufs.forge().alloc("Notify")); + _notify_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.time_Position)); + _ports->at(0) = _notify_port; +} + +void +TimeNode::run(RunContext& context) +{ + BufferRef buf = _notify_port->buffer(0); + LV2_Atom_Sequence* seq = buf->get<LV2_Atom_Sequence>(); + + // Initialise output to the empty sequence + seq->atom.type = _notify_port->bufs().uris().atom_Sequence; + seq->atom.size = sizeof(LV2_Atom_Sequence_Body); + seq->body.unit = 0; + seq->body.pad = 0; + + // Ask the driver to append any time events for this cycle + context.engine().driver()->append_time_events( + context, *_notify_port->buffer(0)); +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/Time.hpp b/src/server/internals/Time.hpp new file mode 100644 index 00000000..1a063f8d --- /dev/null +++ b/src/server/internals/Time.hpp @@ -0,0 +1,59 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_TIME_HPP +#define INGEN_INTERNALS_TIME_HPP + +#include "InternalBlock.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; + +namespace Internals { + +/** Time information block. + * + * This sends messages whenever the transport speed or tempo changes. + * + * \ingroup engine + */ +class TimeNode : public InternalBlock +{ +public: + TimeNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + void run(RunContext& context); + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + OutputPort* _notify_port; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_TIME_HPP diff --git a/src/server/internals/Trigger.cpp b/src/server/internals/Trigger.cpp new file mode 100644 index 00000000..69967877 --- /dev/null +++ b/src/server/internals/Trigger.cpp @@ -0,0 +1,187 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "InputPort.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "RunContext.hpp" +#include "ingen_config.h" +#include "internals/Trigger.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* TriggerNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "Trigger"), Raul::Symbol("trigger")); +} + +TriggerNode::TriggerNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, false, parent, srate) + , _learning(false) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(6); + + const Atom zero = bufs.forge().make(0.0f); + + _midi_in_port = new InputPort(bufs, this, Raul::Symbol("input"), 0, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_in_port->set_property(uris.lv2_name, bufs.forge().alloc("Input")); + _midi_in_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(0) = _midi_in_port; + + _midi_out_port = new OutputPort(bufs, this, Raul::Symbol("event"), 1, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_out_port->set_property(uris.lv2_name, bufs.forge().alloc("Event")); + _midi_out_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(1) = _midi_out_port; + + _note_port = new InputPort(bufs, this, Raul::Symbol("note"), 2, 1, + PortType::ATOM, uris.atom_Sequence, + bufs.forge().make(60.0f)); + _note_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _note_port->set_property(uris.lv2_minimum, zero); + _note_port->set_property(uris.lv2_maximum, bufs.forge().make(127.0f)); + _note_port->set_property(uris.lv2_portProperty, uris.lv2_integer); + _note_port->set_property(uris.lv2_name, bufs.forge().alloc("Note")); + _ports->at(2) = _note_port; + + _gate_port = new OutputPort(bufs, this, Raul::Symbol("gate"), 3, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _gate_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _gate_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _gate_port->set_property(uris.lv2_name, bufs.forge().alloc("Gate")); + _ports->at(3) = _gate_port; + + _trig_port = new OutputPort(bufs, this, Raul::Symbol("trigger"), 4, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _trig_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _trig_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _trig_port->set_property(uris.lv2_name, bufs.forge().alloc("Trigger")); + _ports->at(4) = _trig_port; + + _vel_port = new OutputPort(bufs, this, Raul::Symbol("velocity"), 5, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _vel_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _vel_port->set_property(uris.lv2_minimum, zero); + _vel_port->set_property(uris.lv2_maximum, bufs.forge().make(1.0f)); + _vel_port->set_property(uris.lv2_name, bufs.forge().alloc("Velocity")); + _ports->at(5) = _vel_port; +} + +void +TriggerNode::run(RunContext& context) +{ + const BufferRef midi_in = _midi_in_port->buffer(0); + LV2_Atom_Sequence* const seq = midi_in->get<LV2_Atom_Sequence>(); + const BufferRef midi_out = _midi_out_port->buffer(0); + + // Initialise output to the empty sequence + midi_out->prepare_write(context); + + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + const int64_t t = ev->time.frames; + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY(&ev->body); + bool emit = false; + if (ev->body.type == _midi_in_port->bufs().uris().midi_MidiEvent && + ev->body.size >= 3) { + const FrameTime time = context.start() + t; + switch (lv2_midi_message_type(buf)) { + case LV2_MIDI_MSG_NOTE_ON: + if (buf[2] == 0) { + emit = note_off(context, buf[1], time); + } else { + emit = note_on(context, buf[1], buf[2], time); + } + break; + case LV2_MIDI_MSG_NOTE_OFF: + emit = note_off(context, buf[1], time); + break; + case LV2_MIDI_MSG_CONTROLLER: + switch (buf[1]) { + case LV2_MIDI_CTL_ALL_NOTES_OFF: + case LV2_MIDI_CTL_ALL_SOUNDS_OFF: + _gate_port->set_control_value(context, time, 0.0f); + emit = true; + } + default: + break; + } + } + + if (emit) { + midi_out->append_event(t, &ev->body); + } + } +} + +bool +TriggerNode::note_on(RunContext& context, uint8_t note_num, uint8_t velocity, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + const uint32_t offset = time - context.start(); + + if (_learning) { + _note_port->set_control_value(context, time, (float)note_num); + _note_port->force_monitor_update(); + _learning = false; + } + + if (note_num == lrintf(_note_port->buffer(0)->value_at(offset))) { + _gate_port->set_control_value(context, time, 1.0f); + _trig_port->set_control_value(context, time, 1.0f); + _trig_port->set_control_value(context, time + 1, 0.0f); + _vel_port->set_control_value(context, time, velocity / 127.0f); + return true; + } + return false; +} + +bool +TriggerNode::note_off(RunContext& context, uint8_t note_num, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + const uint32_t offset = time - context.start(); + + if (note_num == lrintf(_note_port->buffer(0)->value_at(offset))) { + _gate_port->set_control_value(context, time, 0.0f); + return true; + } + + return false; +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/Trigger.hpp b/src/server/internals/Trigger.hpp new file mode 100644 index 00000000..4d67395a --- /dev/null +++ b/src/server/internals/Trigger.hpp @@ -0,0 +1,75 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_TRIGGER_HPP +#define INGEN_INTERNALS_TRIGGER_HPP + +#include "InternalBlock.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; + +namespace Internals { + +/** MIDI trigger input block. + * + * Just has a gate, for drums etc. A control port is used to select + * which note number is responded to. + * + * Note that this block is always monophonic, the poly parameter is ignored. + * (Should that change?) + * + * \ingroup engine + */ +class TriggerNode : public InternalBlock +{ +public: + TriggerNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + void run(RunContext& context); + + bool note_on(RunContext& context, uint8_t note_num, uint8_t velocity, FrameTime time); + bool note_off(RunContext& context, uint8_t note_num, FrameTime time); + + void learn() { _learning = true; } + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + bool _learning; + + InputPort* _midi_in_port; + OutputPort* _midi_out_port; + InputPort* _note_port; + OutputPort* _gate_port; + OutputPort* _trig_port; + OutputPort* _vel_port; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_TRIGGER_HPP diff --git a/src/server/jackey.h b/src/server/jackey.h new file mode 100644 index 00000000..fc31d73c --- /dev/null +++ b/src/server/jackey.h @@ -0,0 +1,72 @@ +/* + Copyright 2014-2015 David Robillard <http://drobilla.net> + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +/** + The supported event types of an event port. + + This is a kludge around Jack only supporting MIDI, particularly for OSC. + This property is a comma-separated list of event types, currently "MIDI" or + "OSC". If this contains "OSC", the port may carry OSC bundles (first byte + '#') or OSC messages (first byte '/'). Note that the "status byte" of both + OSC events is not a valid MIDI status byte, so MIDI clients that check the + status byte will gracefully ignore OSC messages if the user makes an + inappropriate connection. +*/ +#define JACKEY_EVENT_TYPES "http://jackaudio.org/metadata/event-types" + +/** + The type of an audio signal. + + This property allows audio ports to be tagged with a "meaning". The value + is a simple string. Currently, the only type is "CV", for "control voltage" + ports. Hosts SHOULD be take care to not treat CV ports as audibile and send + their output directly to speakers. In particular, CV ports are not + necessarily periodic at all and may have very high DC. +*/ +#define JACKEY_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type" + +/** + The name of the icon for the subject (typically client). + + This is used for looking up icons on the system, possibly with many sizes or + themes. Icons should be searched for according to the freedesktop Icon + Theme Specification: + + http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html +*/ +#define JACKEY_ICON_NAME "http://jackaudio.org/metadata/icon-name" + +/** + Channel designation for a port. + + This allows ports to be tagged with a meaningful designation like "left", + "right", "lfe", etc. + + The value MUST be a URI. An extensive set of URIs for designating audio + channels can be found at http://lv2plug.in/ns/ext/port-groups +*/ +#define JACKEY_DESIGNATION "http://lv2plug.in/ns/lv2core#designation" + +/** + Order for a port. + + This is used to specify the best order to show ports in user interfaces. + The value MUST be an integer. There are no other requirements, so there may + be gaps in the orders for several ports. Applications should compare the + orders of ports to determine their relative order, but must not assign any + other relevance to order values. +*/ +#define JACKEY_ORDER "http://jackaudio.org/metadata/order" diff --git a/src/server/mix.cpp b/src/server/mix.cpp new file mode 100644 index 00000000..3e7634fe --- /dev/null +++ b/src/server/mix.cpp @@ -0,0 +1,112 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "Buffer.hpp" +#include "RunContext.hpp" +#include "mix.hpp" + +namespace Ingen { +namespace Server { + +static inline bool +is_end(const Buffer* buf, const LV2_Atom_Event* ev) +{ + const LV2_Atom* atom = buf->get<const LV2_Atom>(); + return lv2_atom_sequence_is_end( + (const LV2_Atom_Sequence_Body*)LV2_ATOM_BODY_CONST(atom), + atom->size, + ev); +} + +void +mix(const RunContext& context, + Buffer* dst, + const Buffer*const* srcs, + uint32_t num_srcs) +{ + if (num_srcs == 1) { + dst->copy(context, srcs[0]); + } else if (dst->is_control()) { + Sample* const out = dst->samples(); + out[0] = srcs[0]->value_at(0); + for (uint32_t i = 1; i < num_srcs; ++i) { + out[0] += srcs[i]->value_at(0); + } + } else if (dst->is_audio()) { + // Copy the first source + dst->copy(context, srcs[0]); + + // Mix in the rest + Sample* __restrict const out = dst->samples(); + const SampleCount end = context.nframes(); + for (uint32_t i = 1; i < num_srcs; ++i) { + const Sample* __restrict const in = srcs[i]->samples(); + if (srcs[i]->is_control()) { // control => audio + for (SampleCount i = 0; i < end; ++i) { + out[i] += in[0]; + } + } else if (srcs[i]->is_audio()) { // audio => audio + for (SampleCount i = 0; i < end; ++i) { + out[i] += in[i]; + } + } else if (srcs[i]->is_sequence()) { // sequence => audio + dst->render_sequence(context, srcs[i], true); + } + } + } else if (dst->is_sequence()) { + const LV2_Atom_Event* iters[num_srcs]; + for (uint32_t i = 0; i < num_srcs; ++i) { + iters[i] = nullptr; + if (srcs[i]->is_sequence()) { + const LV2_Atom_Sequence* seq = srcs[i]->get<const LV2_Atom_Sequence>(); + iters[i] = lv2_atom_sequence_begin(&seq->body); + if (is_end(srcs[i], iters[i])) { + iters[i] = nullptr; + } + } + } + + while (true) { + const LV2_Atom_Event* first = nullptr; + uint32_t first_i = 0; + for (uint32_t i = 0; i < num_srcs; ++i) { + const LV2_Atom_Event* const ev = iters[i]; + if (!first || (ev && ev->time.frames < first->time.frames)) { + first = ev; + first_i = i; + } + } + + if (first) { + dst->append_event( + first->time.frames, first->body.size, first->body.type, + (const uint8_t*)LV2_ATOM_BODY_CONST(&first->body)); + + iters[first_i] = lv2_atom_sequence_next(first); + if (is_end(srcs[first_i], iters[first_i])) { + iters[first_i] = nullptr; + } + } else { + break; + } + } + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/mix.hpp b/src/server/mix.hpp new file mode 100644 index 00000000..3d8880db --- /dev/null +++ b/src/server/mix.hpp @@ -0,0 +1,40 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_MIX_HPP +#define INGEN_ENGINE_MIX_HPP + +#include <cstdint> + +namespace Ingen { + +class URIs; + +namespace Server { + +class Buffer; +class RunContext; + +void +mix(const RunContext& context, + Buffer* dst, + const Buffer*const* srcs, + uint32_t num_srcs); + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_MIX_HPP diff --git a/src/server/types.hpp b/src/server/types.hpp new file mode 100644 index 00000000..e7dae117 --- /dev/null +++ b/src/server/types.hpp @@ -0,0 +1,27 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_TYPES_HPP +#define INGEN_ENGINE_TYPES_HPP + +#include <cstdint> + +typedef float Sample; +typedef uint32_t SampleCount; +typedef uint32_t SampleRate; +typedef uint32_t FrameTime; + +#endif // INGEN_ENGINE_TYPES_HPP diff --git a/src/server/util.hpp b/src/server/util.hpp new file mode 100644 index 00000000..7d30cc8f --- /dev/null +++ b/src/server/util.hpp @@ -0,0 +1,63 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or 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 Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_UTIL_HPP +#define INGEN_ENGINE_UTIL_HPP + +#include <cstdlib> + +#include "ingen/Log.hpp" +#include "raul/Path.hpp" + +#include "ingen_config.h" + +#include <fenv.h> +#ifdef __SSE__ +#include <xmmintrin.h> +#endif + +#ifdef __clang__ +# define REALTIME __attribute__((annotate("realtime"))) +#else +# define REALTIME +#endif + +#if defined(INGEN_HAVE_THREAD_LOCAL) +# define INGEN_THREAD_LOCAL thread_local +#elif defined(INGEN_HAVE_THREAD_BUILTIN) +# define INGEN_THREAD_LOCAL __thread +#else +# define INGEN_THREAD_LOCAL +#endif + +namespace Ingen { +namespace Server { + +/** Set flags to disable denormal processing. + */ +inline void +set_denormal_flags(Ingen::Log& log) +{ +#ifdef __SSE__ + _mm_setcsr(_mm_getcsr() | 0x8040); + log.info("Set SSE denormal-are-zero and flush-to-zero flags\n"); +#endif +} + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_UTIL_HPP diff --git a/src/server/wscript b/src/server/wscript new file mode 100644 index 00000000..8d1ec90d --- /dev/null +++ b/src/server/wscript @@ -0,0 +1,104 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf + +def build(bld): + core_source = ''' + ArcImpl.cpp + BlockFactory.cpp + BlockImpl.cpp + Broadcaster.cpp + Buffer.cpp + BufferFactory.cpp + CompiledGraph.cpp + ClientUpdate.cpp + ControlBindings.cpp + DuplexPort.cpp + Engine.cpp + EventWriter.cpp + GraphImpl.cpp + InputPort.cpp + InternalBlock.cpp + InternalPlugin.cpp + LV2Block.cpp + LV2Plugin.cpp + NodeImpl.cpp + PortImpl.cpp + PostProcessor.cpp + PreProcessor.cpp + RunContext.cpp + SocketListener.cpp + Task.cpp + UndoStack.cpp + Worker.cpp + events/Connect.cpp + events/Copy.cpp + events/CreateBlock.cpp + events/CreateGraph.cpp + events/CreatePort.cpp + events/Delete.cpp + events/Delta.cpp + events/Disconnect.cpp + events/DisconnectAll.cpp + events/Get.cpp + events/Mark.cpp + events/Move.cpp + events/SetPortValue.cpp + events/Undo.cpp + ingen_engine.cpp + internals/BlockDelay.cpp + internals/Controller.cpp + internals/Note.cpp + internals/Time.cpp + internals/Trigger.cpp + mix.cpp + ''' + + obj = bld(features = 'cxx cxxshlib', + source = core_source, + export_includes = ['../..'], + includes = ['.', '../..'], + name = 'libingen_server', + target = 'ingen_server', + install_path = '${LIBDIR}', + use = 'libingen libingen_socket', + cxxflags = bld.env.PTHREAD_CFLAGS + bld.env.INGEN_TEST_CXXFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS + bld.env.INGEN_TEST_LINKFLAGS) + core_libs = 'LV2 LILV RAUL SERD SORD' + autowaf.use_lib(bld, obj, core_libs) + + if bld.env.HAVE_JACK: + obj = bld(features = 'cxx cxxshlib', + source = 'JackDriver.cpp ingen_jack.cpp', + includes = ['.', '../..'], + name = 'libingen_jack', + target = 'ingen_jack', + install_path = '${LIBDIR}', + use = 'libingen_server', + cxxflags = bld.env.PTHREAD_CFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS) + autowaf.use_lib(bld, obj, core_libs + ' JACK') + + if bld.env.HAVE_PORTAUDIO: + obj = bld(features = 'cxx cxxshlib', + source = 'PortAudioDriver.cpp ingen_portaudio.cpp', + includes = ['.', '../..'], + name = 'libingen_portaudio', + target = 'ingen_portaudio', + install_path = '${LIBDIR}', + use = 'libingen_server', + cxxflags = bld.env.PTHREAD_CFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS) + autowaf.use_lib(bld, obj, core_libs + ' PORTAUDIO') + + # Ingen LV2 wrapper + if bld.env.INGEN_BUILD_LV2: + obj = bld(features = 'cxx cxxshlib', + source = ' ingen_lv2.cpp ', + includes = ['.', '../..'], + name = 'libingen_lv2', + target = 'ingen_lv2', + install_path = '${LV2DIR}/ingen.lv2/', + use = 'libingen libingen_server', + cxxflags = bld.env.PTHREAD_CFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS) + autowaf.use_lib(bld, obj, core_libs) diff --git a/src/wscript b/src/wscript new file mode 100644 index 00000000..07379b83 --- /dev/null +++ b/src/wscript @@ -0,0 +1,46 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf + +def build(bld): + sources = [ + 'AtomReader.cpp', + 'AtomWriter.cpp', + 'ClashAvoider.cpp', + 'ColorContext.cpp', + 'Configuration.cpp', + 'FilePath.cpp', + 'Forge.cpp', + 'LV2Features.cpp', + 'Library.cpp', + 'Log.cpp', + 'Parser.cpp', + 'Resource.cpp', + 'Serialiser.cpp', + 'Store.cpp', + 'StreamWriter.cpp', + 'TurtleWriter.cpp', + 'URI.cpp', + 'URIMap.cpp', + 'URIs.cpp', + 'World.cpp', + 'runtime_paths.cpp' + ] + if bld.is_defined('HAVE_SOCKET'): + sources += [ 'SocketReader.cpp', 'SocketWriter.cpp' ] + + lib = [] + if bld.is_defined('HAVE_LIBDL'): + lib += ['dl'] + + obj = bld(features = 'cxx cxxshlib', + source = sources, + export_includes = ['..'], + includes = ['..'], + name = 'libingen', + target = 'ingen', + vnum = '0.0.0', + install_path = '${LIBDIR}', + lib = lib, + cxxflags = bld.env.PTHREAD_CFLAGS + bld.env.INGEN_TEST_CXXFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS + bld.env.INGEN_TEST_LINKFLAGS) + autowaf.use_lib(bld, obj, 'LV2 LILV RAUL SERD SORD SRATOM') |