summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/AtomReader.cpp384
-rw-r--r--src/AtomWriter.cpp652
-rw-r--r--src/ClashAvoider.cpp181
-rw-r--r--src/ColorContext.cpp44
-rw-r--r--src/Configuration.cpp386
-rw-r--r--src/FilePath.cpp242
-rw-r--r--src/Forge.cpp69
-rw-r--r--src/LV2Features.cpp83
-rw-r--r--src/Library.cpp56
-rw-r--r--src/Log.cpp162
-rw-r--r--src/Parser.cpp701
-rw-r--r--src/Resource.cpp238
-rw-r--r--src/Serialiser.cpp582
-rw-r--r--src/SocketReader.cpp199
-rw-r--r--src/SocketWriter.cpp61
-rw-r--r--src/Store.cpp143
-rw-r--r--src/StreamWriter.cpp39
-rw-r--r--src/TurtleWriter.cpp101
-rw-r--r--src/URI.cpp113
-rw-r--r--src/URIMap.cpp123
-rw-r--r--src/URIs.cpp204
-rw-r--r--src/World.cpp355
-rw-r--r--src/client/BlockModel.cpp285
-rw-r--r--src/client/ClientStore.cpp487
-rw-r--r--src/client/GraphModel.cpp176
-rw-r--r--src/client/ObjectModel.cpp108
-rw-r--r--src/client/PluginModel.cpp360
-rw-r--r--src/client/PluginUI.cpp336
-rw-r--r--src/client/PortModel.cpp78
-rw-r--r--src/client/ingen_client.cpp34
-rw-r--r--src/client/wscript23
-rw-r--r--src/gui/App.cpp499
-rw-r--r--src/gui/App.hpp196
-rw-r--r--src/gui/Arc.cpp44
-rw-r--r--src/gui/Arc.hpp52
-rw-r--r--src/gui/BreadCrumbs.cpp229
-rw-r--r--src/gui/BreadCrumbs.hpp119
-rw-r--r--src/gui/ConnectWindow.cpp572
-rw-r--r--src/gui/ConnectWindow.hpp116
-rw-r--r--src/gui/GraphBox.cpp922
-rw-r--r--src/gui/GraphBox.hpp213
-rw-r--r--src/gui/GraphCanvas.cpp912
-rw-r--r--src/gui/GraphCanvas.hpp159
-rw-r--r--src/gui/GraphPortModule.cpp166
-rw-r--r--src/gui/GraphPortModule.hpp79
-rw-r--r--src/gui/GraphTreeWindow.cpp235
-rw-r--r--src/gui/GraphTreeWindow.hpp123
-rw-r--r--src/gui/GraphView.cpp154
-rw-r--r--src/gui/GraphView.hpp98
-rw-r--r--src/gui/GraphWindow.cpp85
-rw-r--r--src/gui/GraphWindow.hpp80
-rw-r--r--src/gui/LoadGraphWindow.cpp257
-rw-r--r--src/gui/LoadGraphWindow.hpp95
-rw-r--r--src/gui/LoadPluginWindow.cpp511
-rw-r--r--src/gui/LoadPluginWindow.hpp160
-rw-r--r--src/gui/MessagesWindow.cpp141
-rw-r--r--src/gui/MessagesWindow.hpp70
-rw-r--r--src/gui/NewSubgraphWindow.cpp119
-rw-r--r--src/gui/NewSubgraphWindow.hpp72
-rw-r--r--src/gui/NodeMenu.cpp253
-rw-r--r--src/gui/NodeMenu.hpp75
-rw-r--r--src/gui/NodeModule.cpp518
-rw-r--r--src/gui/NodeModule.hpp104
-rw-r--r--src/gui/ObjectMenu.cpp145
-rw-r--r--src/gui/ObjectMenu.hpp77
-rw-r--r--src/gui/PluginMenu.cpp176
-rw-r--r--src/gui/PluginMenu.hpp80
-rw-r--r--src/gui/Port.cpp534
-rw-r--r--src/gui/Port.hpp102
-rw-r--r--src/gui/PortMenu.cpp174
-rw-r--r--src/gui/PortMenu.hpp66
-rw-r--r--src/gui/PropertiesWindow.cpp591
-rw-r--r--src/gui/PropertiesWindow.hpp129
-rw-r--r--src/gui/RDFS.cpp259
-rw-r--r--src/gui/RDFS.hpp80
-rw-r--r--src/gui/RenameWindow.cpp137
-rw-r--r--src/gui/RenameWindow.hpp64
-rw-r--r--src/gui/Style.cpp106
-rw-r--r--src/gui/Style.hpp56
-rw-r--r--src/gui/SubgraphModule.cpp102
-rw-r--r--src/gui/SubgraphModule.hpp64
-rw-r--r--src/gui/ThreadedLoader.cpp148
-rw-r--r--src/gui/ThreadedLoader.hpp96
-rw-r--r--src/gui/URIEntry.cpp192
-rw-r--r--src/gui/URIEntry.hpp68
-rw-r--r--src/gui/WidgetFactory.cpp80
-rw-r--r--src/gui/WidgetFactory.hpp58
-rw-r--r--src/gui/Window.hpp78
-rw-r--r--src/gui/WindowFactory.cpp302
-rw-r--r--src/gui/WindowFactory.hpp99
-rw-r--r--src/gui/ingen_gui.cpp67
-rw-r--r--src/gui/ingen_gui.gladep9
-rw-r--r--src/gui/ingen_gui.ui3049
-rw-r--r--src/gui/ingen_gui_lv2.cpp209
-rw-r--r--src/gui/ingen_style.rc155
-rw-r--r--src/gui/rgba.hpp58
-rw-r--r--src/gui/wscript111
-rw-r--r--src/ingen/ingen.cpp264
-rw-r--r--src/ingen/ingen.desktop9
-rw-r--r--src/ingen/ingen.grind5
-rw-r--r--src/runtime_paths.cpp146
-rw-r--r--src/server/ArcImpl.cpp114
-rw-r--r--src/server/ArcImpl.hpp84
-rw-r--r--src/server/BlockFactory.cpp229
-rw-r--r--src/server/BlockFactory.hpp67
-rw-r--r--src/server/BlockImpl.cpp303
-rw-r--r--src/server/BlockImpl.hpp208
-rw-r--r--src/server/Broadcaster.cpp97
-rw-r--r--src/server/Broadcaster.hpp118
-rw-r--r--src/server/Buffer.cpp468
-rw-r--r--src/server/Buffer.hpp244
-rw-r--r--src/server/BufferFactory.cpp190
-rw-r--r--src/server/BufferFactory.hpp118
-rw-r--r--src/server/BufferRef.hpp38
-rw-r--r--src/server/ClientUpdate.cpp155
-rw-r--r--src/server/ClientUpdate.hpp80
-rw-r--r--src/server/CompiledGraph.cpp274
-rw-r--r--src/server/CompiledGraph.hpp84
-rw-r--r--src/server/ControlBindings.cpp431
-rw-r--r--src/server/ControlBindings.hpp148
-rw-r--r--src/server/DirectDriver.hpp110
-rw-r--r--src/server/Driver.hpp110
-rw-r--r--src/server/DuplexPort.cpp236
-rw-r--r--src/server/DuplexPort.hpp100
-rw-r--r--src/server/Engine.cpp526
-rw-r--r--src/server/Engine.hpp221
-rw-r--r--src/server/EnginePort.hpp66
-rw-r--r--src/server/Event.hpp163
-rw-r--r--src/server/EventWriter.cpp147
-rw-r--r--src/server/EventWriter.hpp86
-rw-r--r--src/server/FrameTimer.hpp110
-rw-r--r--src/server/GraphImpl.cpp379
-rw-r--r--src/server/GraphImpl.hpp200
-rw-r--r--src/server/GraphPlugin.hpp63
-rw-r--r--src/server/InputPort.cpp261
-rw-r--r--src/server/InputPort.hpp130
-rw-r--r--src/server/InternalBlock.cpp73
-rw-r--r--src/server/InternalBlock.hpp48
-rw-r--r--src/server/InternalPlugin.cpp67
-rw-r--r--src/server/InternalPlugin.hpp57
-rw-r--r--src/server/JackDriver.cpp584
-rw-r--r--src/server/JackDriver.hpp172
-rw-r--r--src/server/LV2Block.cpp742
-rw-r--r--src/server/LV2Block.hpp152
-rw-r--r--src/server/LV2Options.hpp71
-rw-r--r--src/server/LV2Plugin.cpp143
-rw-r--r--src/server/LV2Plugin.hpp72
-rw-r--r--src/server/LV2ResizeFeature.hpp65
-rw-r--r--src/server/Load.hpp57
-rw-r--r--src/server/NodeImpl.cpp50
-rw-r--r--src/server/NodeImpl.hpp109
-rw-r--r--src/server/OutputPort.hpp51
-rw-r--r--src/server/PluginImpl.hpp96
-rw-r--r--src/server/PortAudioDriver.cpp297
-rw-r--r--src/server/PortAudioDriver.hpp132
-rw-r--r--src/server/PortImpl.cpp569
-rw-r--r--src/server/PortImpl.hpp312
-rw-r--r--src/server/PortType.hpp91
-rw-r--r--src/server/PostProcessor.cpp114
-rw-r--r--src/server/PostProcessor.hpp74
-rw-r--r--src/server/PreProcessContext.hpp84
-rw-r--r--src/server/PreProcessor.cpp248
-rw-r--r--src/server/PreProcessor.hpp87
-rw-r--r--src/server/RunContext.cpp195
-rw-r--r--src/server/RunContext.hpp161
-rw-r--r--src/server/SocketListener.cpp190
-rw-r--r--src/server/SocketListener.hpp41
-rw-r--r--src/server/SocketServer.hpp80
-rw-r--r--src/server/Task.cpp158
-rw-r--r--src/server/Task.hpp120
-rw-r--r--src/server/ThreadManager.hpp68
-rw-r--r--src/server/UndoStack.cpp253
-rw-r--r--src/server/UndoStack.hpp107
-rw-r--r--src/server/Worker.cpp163
-rw-r--r--src/server/Worker.hpp76
-rw-r--r--src/server/events.hpp35
-rw-r--r--src/server/events/Connect.cpp188
-rw-r--r--src/server/events/Connect.hpp74
-rw-r--r--src/server/events/Copy.cpp216
-rw-r--r--src/server/events/Copy.hpp68
-rw-r--r--src/server/events/CreateBlock.cpp180
-rw-r--r--src/server/events/CreateBlock.hpp66
-rw-r--r--src/server/events/CreateGraph.cpp236
-rw-r--r--src/server/events/CreateGraph.hpp74
-rw-r--r--src/server/events/CreatePort.cpp219
-rw-r--r--src/server/events/CreatePort.hpp82
-rw-r--r--src/server/events/Delete.cpp216
-rw-r--r--src/server/events/Delete.hpp86
-rw-r--r--src/server/events/Delta.cpp670
-rw-r--r--src/server/events/Delta.hpp133
-rw-r--r--src/server/events/Disconnect.cpp224
-rw-r--r--src/server/events/Disconnect.hpp87
-rw-r--r--src/server/events/DisconnectAll.cpp176
-rw-r--r--src/server/events/DisconnectAll.hpp78
-rw-r--r--src/server/events/Get.cpp111
-rw-r--r--src/server/events/Get.hpp65
-rw-r--r--src/server/events/Mark.cpp112
-rw-r--r--src/server/events/Mark.hpp69
-rw-r--r--src/server/events/Move.cpp91
-rw-r--r--src/server/events/Move.hpp57
-rw-r--r--src/server/events/SetPortValue.cpp139
-rw-r--r--src/server/events/SetPortValue.hpp71
-rw-r--r--src/server/events/Undo.cpp85
-rw-r--r--src/server/events/Undo.hpp58
-rw-r--r--src/server/ingen_engine.cpp44
-rw-r--r--src/server/ingen_jack.cpp58
-rw-r--r--src/server/ingen_lv2.cpp848
-rw-r--r--src/server/ingen_portaudio.cpp54
-rw-r--r--src/server/internals/BlockDelay.cpp89
-rw-r--r--src/server/internals/BlockDelay.hpp62
-rw-r--r--src/server/internals/Controller.cpp174
-rw-r--r--src/server/internals/Controller.hpp71
-rw-r--r--src/server/internals/Note.cpp420
-rw-r--r--src/server/internals/Note.hpp109
-rw-r--r--src/server/internals/Time.cpp78
-rw-r--r--src/server/internals/Time.hpp59
-rw-r--r--src/server/internals/Trigger.cpp187
-rw-r--r--src/server/internals/Trigger.hpp75
-rw-r--r--src/server/jackey.h72
-rw-r--r--src/server/mix.cpp112
-rw-r--r--src/server/mix.hpp40
-rw-r--r--src/server/types.hpp27
-rw-r--r--src/server/util.hpp63
-rw-r--r--src/server/wscript104
-rw-r--r--src/wscript46
225 files changed, 41923 insertions, 0 deletions
diff --git a/src/AtomReader.cpp b/src/AtomReader.cpp
new file mode 100644
index 00000000..cc47498f
--- /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/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,
+ nullptr);
+
+ 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,
+ nullptr);
+
+ 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,
+ nullptr);
+ 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..9c7da98e
--- /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..dbcc1a7e
--- /dev/null
+++ b/src/ClashAvoider.cpp
@@ -0,0 +1,181 @@
+/*
+ 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 <cassert>
+#include <cctype>
+#include <cstdio>
+#include <sstream>
+#include <string>
+#include <utility>
+
+#include <boost/optional.hpp>
+
+#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();
+}
+
+static boost::optional<size_t>
+numeric_suffix_start(const std::string& str)
+{
+ if (!isdigit(str[str.length() - 1])) {
+ return {};
+ }
+
+ size_t i = str.length() - 1;
+ while (i > 0 && isdigit(str[i - 1])) {
+ --i;
+ }
+
+ return i;
+}
+
+std::string
+ClashAvoider::adjust_name(const Raul::Path& old_path,
+ const Raul::Path& new_path,
+ std::string name)
+{
+ const auto name_suffix_start = numeric_suffix_start(name);
+ if (!name_suffix_start) {
+ return name; // No numeric suffix, just re-use old label
+ }
+
+ const auto name_suffix = atoi(name.c_str() + *name_suffix_start);
+ const auto old_suffix_start = numeric_suffix_start(old_path);
+ const auto new_suffix_start = numeric_suffix_start(new_path);
+ if (old_suffix_start && new_suffix_start) {
+ // Add the offset applied to the symbol to the label suffix
+ const auto old_suffix = atoi(old_path.c_str() + *old_suffix_start);
+ const auto new_suffix = atoi(new_path.c_str() + *new_suffix_start);
+ const auto offset = new_suffix - old_suffix;
+ return (name.substr(0, *name_suffix_start) +
+ std::to_string(name_suffix + offset));
+ } else {
+ // Add 1 to previous label suffix
+ return (name.substr(0, *name_suffix_start) +
+ std::to_string(name_suffix + 1));
+ }
+}
+
+} // namespace ingen
diff --git a/src/ColorContext.cpp b/src/ColorContext.cpp
new file mode 100644
index 00000000..9794e8ce
--- /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..c05c516e
--- /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..897dea7d
--- /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..9ac406fc
--- /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/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..b63ed980
--- /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..48002ef0
--- /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..c1f8cb0a
--- /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..9318d44a
--- /dev/null
+++ b/src/Parser.cpp
@@ -0,0 +1,701 @@
+/*
+ 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/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,
+ boost::optional<Properties> data = {})
+{
+ 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);
+
+ // Replace any properties given in `data`
+ if (data) {
+ for (const auto& prop : *data) {
+ if (ctx == Resource::Graph::DEFAULT ||
+ prop.second.context() == ctx) {
+ props.erase(prop.first);
+ }
+ }
+
+ for (const auto& prop : *data) {
+ if (ctx == Resource::Graph::DEFAULT ||
+ prop.second.context() == ctx) {
+ props.emplace(prop);
+ }
+ }
+ }
+
+ 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_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()), data);
+
+ parse_graph(world, target, model, base_uri,
+ subject, Resource::Graph::EXTERNAL,
+ path.parent(), Raul::Symbol(path.symbol()), data);
+ } else {
+ // Prototype is non-file URI, plugin
+ Properties props = get_properties(
+ world, model, subject, Resource::Graph::DEFAULT, data);
+ 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, data);
+ 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 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;
+ const Properties properties = get_properties(
+ world, model, s, Resource::Graph::DEFAULT, data);
+ target->put(path_to_uri(path), properties);
+ 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..86403388
--- /dev/null
+++ b/src/Resource.cpp
@@ -0,0 +1,238 @@
+/*
+ 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
+{
+ if (ctx == Resource::Graph::DEFAULT) {
+ return properties();
+ }
+
+ 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..36d4a87f
--- /dev/null
+++ b/src/Serialiser.cpp
@@ -0,0 +1,582 @@
+/*
+ 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/state/state.h"
+#include "lv2/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, Resource::Graph context)
+{
+ 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(), context, 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..de2f3342
--- /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..e927666c
--- /dev/null
+++ b/src/SocketWriter.cpp
@@ -0,0 +1,61 @@
+/*
+ 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 <boost/variant/get.hpp>
+
+#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))
+{}
+
+void
+SocketWriter::message(const Message& message)
+{
+ TurtleWriter::message(message);
+ if (boost::get<BundleEnd>(&message)) {
+ // Send a null byte to indicate end of bundle
+ const char end[] = { 0 };
+ send(_socket->fd(), end, 1, MSG_NOSIGNAL);
+ }
+}
+
+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;
+}
+
+} // namespace ingen
diff --git a/src/Store.cpp b/src/Store.cpp
new file mode 100644
index 00000000..0bbeea30
--- /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..631369b6
--- /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..9bceaa06
--- /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..8f71b70c
--- /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(),
+ nullptr,
+ &_uri))
+{}
+
+URI::URI(const char* str)
+ : _node(serd_node_new_uri_from_string((const uint8_t*)str, nullptr, &_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, nullptr, &_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(),
+ nullptr,
+ &_uri,
+ true))
+{}
+
+URI::URI(const URI& uri)
+ : _node(serd_node_new_uri(&uri._uri, nullptr, &_uri))
+{}
+
+URI&
+URI::operator=(const URI& uri)
+{
+ serd_node_free(&_node);
+ _node = serd_node_new_uri(&uri._uri, nullptr, &_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, nullptr, &uri);
+ return URI(node, uri);
+}
+
+} // namespace ingen
diff --git a/src/URIMap.cpp b/src/URIMap.cpp
new file mode 100644
index 00000000..7c5774f8
--- /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()
+ : nullptr);
+}
+
+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..90f351b1
--- /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/atom/atom.h"
+#include "lv2/buf-size/buf-size.h"
+#include "lv2/core/lv2.h"
+#include "lv2/log/log.h"
+#include "lv2/midi/midi.h"
+#include "lv2/morph/morph.h"
+#include "lv2/options/options.h"
+#include "lv2/parameters/parameters.h"
+#include "lv2/patch/patch.h"
+#include "lv2/port-props/port-props.h"
+#include "lv2/presets/presets.h"
+#include "lv2/resize-port/resize-port.h"
+#include "lv2/state/state.h"
+#include "lv2/time/time.h"
+#include "lv2/worker/worker.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..2daff6a6
--- /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..a15674d4
--- /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..ad3b3c0d
--- /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..31947ba7
--- /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..65dd1a37
--- /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..99269831
--- /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..2368ad63
--- /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/atom/atom.h"
+#include "lv2/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..f83e043b
--- /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..0660b66d
--- /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) override {}
+};
+
+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..8a33fe46
--- /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..76465bfb
--- /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..339a1248
--- /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, nullptr);
+ 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..4eedde88
--- /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..2d2c535e
--- /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..35c97fd0
--- /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..e76fbc06
--- /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..151b8e92
--- /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() override;
+ void on_hide() override;
+
+ 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..966e8b9c
--- /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..93599e0b
--- /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..931cc23f
--- /dev/null
+++ b/src/gui/GraphCanvas.cpp
@@ -0,0 +1,912 @@
+/*
+ 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/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;
+ }
+ 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()));
+
+ // Copy properties, except those that should not be inherited in copies
+ Properties props = node->properties();
+ for (const auto& prop : {uris.lv2_prototype,
+ uris.ingen_canvasX,
+ uris.ingen_canvasY,
+ uris.lv2_index,
+ uris.lv2_symbol}) {
+ props.erase(prop);
+ }
+
+ // Store the old URI as a prototype (keeps provenance around)
+ props.emplace(uris.lv2_prototype, _app.forge().make_urid(old_uri));
+
+ // Adjust numeric suffix on name if appropriate
+ auto n = props.find(uris.lv2_name);
+ if (n != props.end()) {
+ n->second = _app.forge().alloc(ClashAvoider::adjust_name(
+ old_path, new_path, n->second.ptr<char>()));
+ }
+
+ // 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..509b0a4a
--- /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() override;
+ 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..b5b234c4
--- /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..548b7267
--- /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) override;
+
+ 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..2edf8a44
--- /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..0df5d01c
--- /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(nullptr)
+ {}
+
+ void set_window(GraphTreeWindow* win) { _window = win; }
+
+ bool on_button_press_event(GdkEventButton* ev) override {
+ 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..42e19344
--- /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..57a210a3
--- /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..086886ef
--- /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..bdcda312
--- /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) override;
+
+ 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() override;
+ void on_show() override;
+ bool on_key_press_event(GdkEventKey* event) override;
+
+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..a8bbd94a
--- /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..03cb8c05
--- /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() override;
+
+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..743c0f11
--- /dev/null
+++ b/src/gui/LoadPluginWindow.cpp
@@ -0,0 +1,511 @@
+/*
+ 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));
+
+ _search_entry->signal_icon_release().connect(
+ sigc::mem_fun(this, &LoadPluginWindow::name_cleared));
+
+ _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;
+ }
+ }
+}
+
+void
+LoadPluginWindow::name_cleared(Gtk::EntryIconPosition pos, const GdkEventButton* event)
+{
+ _search_entry->set_text("");
+}
+
+/** 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..0e6226c9
--- /dev/null
+++ b/src/gui/LoadPluginWindow.hpp
@@ -0,0 +1,160 @@
+/*
+ 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() override;
+ bool on_key_press_event(GdkEventKey* event) override;
+
+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();
+ void name_cleared(Gtk::EntryIconPosition pos, const GdkEventButton* event);
+
+ 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..2b980a53
--- /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..fae6c2c3
--- /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/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) override;
+
+ 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..4f8197f2
--- /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..403faa42
--- /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..45f1e7cd
--- /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/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..fd4962a9
--- /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() override;
+ 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..8605e91d
--- /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/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..ed5914de
--- /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) override;
+
+ 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) override;
+
+ 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..7c81adf2
--- /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..b0a0cc10
--- /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..e85d1da7
--- /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..e4343ed7
--- /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..60e11471
--- /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..d88f3f9d
--- /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) override;
+
+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) override;
+ 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..3a5b510d
--- /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..60084223
--- /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() override;
+ 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..80565dd5
--- /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..c1c3eee4
--- /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() override;
+
+ 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..4ff26914
--- /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..8de9076d
--- /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..33516c2d
--- /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/core/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..89d90f26
--- /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..3a122e45
--- /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..bb403ffd
--- /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..9c836644
--- /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..6f4e1c4c
--- /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) override;
+
+ void store_location(double ax, double ay) override;
+
+ 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..cd7b1594
--- /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..70e13fce
--- /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..97099b46
--- /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..247c4a6d
--- /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..3ca4eec4
--- /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..e640e2a7
--- /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..5f839578
--- /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) override {
+ 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) override {
+ 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..06c8266a
--- /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..4e71c7cf
--- /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 = nullptr,
+ 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 = nullptr);
+
+ 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..441993f7
--- /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) override {
+ 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) override {
+ 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 &lt;http://drobilla.net&gt;</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 &lt;d@drobilla.net&gt;</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">&lt;b&gt;Graph Search Path: &lt;/b&gt;</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">&lt;i&gt;Example: /foo/bar:/home/user/graphs&lt;/i&gt;</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">&lt;b&gt;Name&lt;/b&gt;</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">&lt;b&gt;Name&lt;/b&gt;</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">&lt;b&gt;Name&lt;/b&gt;</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..251c75e1
--- /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/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) override {
+ _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..f31e958c
--- /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..8e16937d
--- /dev/null
+++ b/src/gui/wscript
@@ -0,0 +1,111 @@
+#!/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', system=True, mandatory=False)
+ autowaf.check_pkg(conf, 'gthread-2.0', uselib_store='GTHREAD',
+ atleast_version='2.14.0', system=True, mandatory=False)
+ autowaf.check_pkg(conf, 'gtkmm-2.4', uselib_store='GTKMM',
+ atleast_version='2.14.0', system=True, 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', system=True, 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..9d099a43
--- /dev/null
+++ b/src/ingen/ingen.cpp
@@ -0,0 +1,264 @@
+/*
+ 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, nullptr, nullptr));
+ 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..15b83084
--- /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..ffdddb8e
--- /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/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(const RunContext&, uint32_t voice) 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..54f88749
--- /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/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 override;
+ const Raul::Path& head_path() const override;
+
+ /** 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(const RunContext& ctx, uint32_t voice) 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..de983df6
--- /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..47309f69
--- /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..17462ab4
--- /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..34045b6d
--- /dev/null
+++ b/src/server/BlockImpl.hpp
@@ -0,0 +1,208 @@
+/*
+ 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();
+
+ GraphType graph_type() const override { 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);
+
+ Node* port(uint32_t index) const override;
+ 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; }
+
+ bool prepare_poly(BufferFactory& bufs, uint32_t poly) override;
+ bool apply_poly(RunContext& context, uint32_t poly) override;
+
+ /** Information about the Plugin this Block is an instance of.
+ * Not the best name - not all blocks come from plugins (ie Graph)
+ */
+ const Resource* plugin() const override;
+
+ /** 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. */
+ GraphImpl* parent_graph() const override { return (GraphImpl*)_parent; }
+
+ uint32_t num_ports() const override { 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..f8b58beb
--- /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..1d5dd16e
--- /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..eef594a3
--- /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/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..d9b4e7ad
--- /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/atom/atom.h"
+#include "lv2/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..c9eeed49
--- /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..e8f05864
--- /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..f3a2ef5c
--- /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..3273c399
--- /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..514997a1
--- /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..67752340
--- /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..664fea85
--- /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..94a12a7e
--- /dev/null
+++ b/src/server/ControlBindings.cpp
@@ -0,0 +1,431 @@
+/*
+ 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/atom/util.h"
+#include "lv2/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,
+ nullptr);
+ 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,
+ nullptr);
+ 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..ea170f81
--- /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/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..b3d49af0
--- /dev/null
+++ b/src/server/DirectDriver.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_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 override { return true; }
+
+ EnginePort* create_port(DuplexPort* graph_port) override {
+ return new EnginePort(graph_port);
+ }
+
+ EnginePort* get_port(const Raul::Path& path) override {
+ for (auto& p : _ports) {
+ if (p.graph_port()->path() == path) {
+ return &p;
+ }
+ }
+
+ return nullptr;
+ }
+
+ void add_port(RunContext& context, EnginePort* port) override {
+ _ports.push_back(*port);
+ }
+
+ void remove_port(RunContext& context, EnginePort* port) override {
+ _ports.erase(_ports.iterator_to(*port));
+ }
+
+ void rename_port(const Raul::Path& old_path,
+ const Raul::Path& new_path) override {}
+
+ void port_property(const Raul::Path& path,
+ const URI& uri,
+ const Atom& value) override {}
+
+ void register_port(EnginePort& port) override {}
+ void unregister_port(EnginePort& port) override {}
+
+ SampleCount block_length() const override { return _block_length; }
+
+ size_t seq_size() const override { return _seq_size; }
+
+ SampleCount sample_rate() const override { return _sample_rate; }
+
+ SampleCount frame_time() const override {
+ return _engine.run_context().start();
+ }
+
+ void append_time_events(RunContext& context, Buffer& buffer) override {}
+
+ int real_time_priority() override { 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..76040395
--- /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..2823da05
--- /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..0edd487f
--- /dev/null
+++ b/src/server/DuplexPort.hpp
@@ -0,0 +1,100 @@
+/*
+ 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) override;
+
+ void on_property(const URI& uri, const Atom& value) override;
+
+ uint32_t max_tail_poly(RunContext& context) const override;
+
+ bool prepare_poly(BufferFactory& bufs, uint32_t poly) override;
+
+ bool apply_poly(RunContext& context, uint32_t poly) override;
+
+ bool get_buffers(BufferFactory& bufs,
+ PortImpl::GetFn get,
+ const MPtr<Voices>& voices,
+ uint32_t poly,
+ size_t num_in_arcs) const override;
+
+ void set_is_driver_port(BufferFactory& bufs) override;
+
+ /** 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) override;
+
+ void pre_process(RunContext& context) override;
+ void post_process(RunContext& context) override;
+
+ SampleCount
+ next_value_offset(SampleCount offset, SampleCount end) const override;
+};
+
+} // 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..b880abf7
--- /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/buf-size/buf-size.h"
+#include "lv2/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..02da0719
--- /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
+ void init(double sample_rate, uint32_t block_length, size_t seq_size) override;
+ bool supports_dynamic_ports() const override;
+ bool activate() override;
+ void deactivate() override;
+ bool pending_events() const override;
+ unsigned run(uint32_t sample_count) override;
+ void quit() override;
+ bool main_iteration() override;
+ void register_client(SPtr<Interface> client) override;
+ bool unregister_client(SPtr<Interface> client) override;
+
+ void listen() override;
+
+ /** 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) override;
+ void advance(SampleCount nframes) override;
+ void locate(FrameTime s, SampleCount nframes) override;
+
+ 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..294bf45a
--- /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..9cd97a35
--- /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..695be88e
--- /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..aad15a76
--- /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..57acbaa5
--- /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..ae9b8f6e
--- /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..384566bf
--- /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();
+
+ GraphType graph_type() const override { return GraphType::GRAPH; }
+
+ BlockImpl* duplicate(Engine& engine,
+ const Raul::Symbol& symbol,
+ GraphImpl* parent) override;
+
+ void activate(BufferFactory& bufs) override;
+ void deactivate() override;
+
+ void pre_process(RunContext& context) override;
+ void process(RunContext& context) override;
+ void run(RunContext& context) override;
+
+ void set_buffer_size(RunContext& context,
+ BufferFactory& bufs,
+ LV2_URID type,
+ uint32_t size) override;
+
+ /** 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..ef741587
--- /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) override
+ {
+ return nullptr;
+ }
+
+ const Raul::Symbol symbol() const override { 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..76382270
--- /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(ctx, 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(context, 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(context, w).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(context, v).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..edb2a3a7
--- /dev/null
+++ b/src/server/InputPort.hpp
@@ -0,0 +1,130 @@
+/*
+ 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) override;
+
+ /** 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) override;
+
+ /** Set up buffer pointers. */
+ void pre_process(RunContext& context) override;
+
+ /** Prepare buffer for access, mixing if necessary. */
+ void pre_run(RunContext& context) override;
+
+ /** Prepare buffer for next process cycle. */
+ void post_process(RunContext& context) override;
+
+ SampleCount
+ next_value_offset(SampleCount offset, SampleCount end) const override;
+
+ size_t num_arcs() const override { 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 override;
+
+ 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..cf57e6b2
--- /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, 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..47c1c09f
--- /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) override;
+
+ void pre_process(RunContext& context) override;
+};
+
+} // 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..49590b24
--- /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..a6998040
--- /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) override;
+
+ const Raul::Symbol symbol() const override { 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..a331811e
--- /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/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..c9307623
--- /dev/null
+++ b/src/server/JackDriver.hpp
@@ -0,0 +1,172 @@
+/*
+ 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/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() override;
+ void deactivate() override;
+
+ bool dynamic_ports() const override { return true; }
+
+ EnginePort* create_port(DuplexPort* graph_port) override;
+ EnginePort* get_port(const Raul::Path& path) override;
+
+ void rename_port(const Raul::Path& old_path, const Raul::Path& new_path) override;
+ void port_property(const Raul::Path& path, const URI& uri, const Atom& value) override;
+ void add_port(RunContext& context, EnginePort* port) override;
+ void remove_port(RunContext& context, EnginePort* port) override;
+ void register_port(EnginePort& port) override;
+ void unregister_port(EnginePort& port) override;
+
+ /** 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) override;
+
+ int real_time_priority() override {
+ return jack_client_real_time_priority(_client);
+ }
+
+ jack_client_t* jack_client() const { return _client; }
+ SampleCount block_length() const override { return _block_length; }
+ size_t seq_size() const override { return _seq_size; }
+ SampleCount sample_rate() const override { return _sample_rate; }
+
+ SampleCount frame_time() const override {
+ 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..c2b8e973
--- /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/morph/morph.h"
+#include "lv2/presets/presets.h"
+#include "lv2/options/options.h"
+#include "lv2/resize-port/resize-port.h"
+#include "lv2/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..8a362fec
--- /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/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() override { return instance(0); }
+ bool save_state(const FilePath& dir) const override;
+
+ BlockImpl* duplicate(Engine& engine,
+ const Raul::Symbol& symbol,
+ GraphImpl* parent) override;
+
+ bool prepare_poly(BufferFactory& bufs, uint32_t poly) override;
+ bool apply_poly(RunContext& context, uint32_t poly) override;
+
+ void activate(BufferFactory& bufs) override;
+ void deactivate() override;
+
+ LV2_Worker_Status work(uint32_t size, const void* data);
+
+ void run(RunContext& context) override;
+ void post_process(RunContext& context) override;
+
+ LilvState* load_preset(const URI& uri) override;
+
+ void apply_state(const UPtr<Worker>& worker, const LilvState* state) override;
+
+ boost::optional<Resource> save_preset(const URI& uri,
+ const Properties& props) override;
+
+ void set_port_buffer(uint32_t voice,
+ uint32_t port_num,
+ BufferRef buf,
+ SampleCount offset) override;
+
+ 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..7a8b3c20
--- /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/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 override { return LV2_OPTIONS__options; }
+
+ SPtr<LV2_Feature> feature(World* w, Node* n) override {
+ 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..4b97e9f1
--- /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/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..9d342f54
--- /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) override;
+
+ const Raul::Symbol symbol() const override;
+
+ World* world() const { return _world; }
+ const LilvPlugin* lilv_plugin() const { return _lilv_plugin; }
+
+ void update_properties() override;
+
+ void load_presets() override;
+
+ URI bundle_uri() const override {
+ 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..4d7dd1f8
--- /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/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..b94a8fed
--- /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..a28d7baf
--- /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..0ac889e0
--- /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 {
+
+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 override { return _symbol; }
+
+ Node* graph_parent() const override { return _parent; }
+ NodeImpl* parent() const { return _parent; }
+
+ /** Rename */
+ void set_path(const Raul::Path& new_path) override {
+ _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 override;
+
+ /** The Graph this object is a child of. */
+ virtual GraphImpl* parent_graph() const;
+
+ const Raul::Path& path() const override { 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..0437c367
--- /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..fd1fcc66
--- /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..f67a62bb
--- /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/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..e50cc8a6
--- /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/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() override;
+ void deactivate() override;
+
+ EnginePort* create_port(DuplexPort* graph_port) override;
+ EnginePort* get_port(const Raul::Path& path) override;
+
+ void rename_port(const Raul::Path& old_path, const Raul::Path& new_path) override;
+ void port_property(const Raul::Path& path, const URI& uri, const Atom& value) override;
+ void add_port(RunContext& context, EnginePort* port) override;
+ void remove_port(RunContext& context, EnginePort* port) override;
+ void register_port(EnginePort& port) override;
+ void unregister_port(EnginePort& port) override;
+
+ void append_time_events(RunContext& context, Buffer& buffer) override {}
+
+ SampleCount frame_time() const override;
+
+ int real_time_priority() override { return 80; }
+
+ SampleCount block_length() const override { return _block_length; }
+ size_t seq_size() const override { return _seq_size; }
+ SampleCount sample_rate() const override { 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..f03939d3
--- /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/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..d9b81a16
--- /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);
+
+ GraphType graph_type() const override { 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.
+ */
+ bool prepare_poly(BufferFactory& bufs, uint32_t poly) override;
+
+ /** Apply a new polyphony value.
+ *
+ * Audio thread.
+ * \a poly Must be < the most recent value passed to prepare_poly.
+ */
+ bool apply_poly(RunContext& context, uint32_t poly) override;
+
+ /** 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..d5fd0238
--- /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/atom/atom.h"
+#include "lv2/core/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..70be6f7b
--- /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) override { return false; }
+ void execute(RunContext& context) override {}
+ void post_process() override {}
+};
+
+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..affb343f
--- /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..1cfa1931
--- /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..dbe7ddbe
--- /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..9ee20255
--- /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..ee272d46
--- /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), &note) != sizeof(note) ||
+ note.time >= end) {
+ return;
+ }
+ if (_event_sink->read(sizeof(note), &note) == 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..15e65806
--- /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/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..9a0ce346
--- /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..b3b5931e
--- /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..bd285e6e
--- /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..63f3ef34
--- /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..97a6101c
--- /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..bf14a423
--- /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..77b4706f
--- /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/atom/util.h"
+#include "lv2/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..6f0ac192
--- /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/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) override;
+ 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..59ef306f
--- /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/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..a2bf4a7d
--- /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/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 override { return LV2_WORKER__schedule; }
+
+ SPtr<LV2_Feature> feature(World* world, Node* n) override;
+
+ 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..99a90a49
--- /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..16ea8bc9
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+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..5d2d511c
--- /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..f31fe4d4
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+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..fabdbd85
--- /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..02bfeda7
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+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..27781cbc
--- /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..1537283e
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+ 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..a79c85ef
--- /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..6c760d29
--- /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/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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+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..ff9f32b4
--- /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..4b0c314b
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+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..7404aea6
--- /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..d36e81aa
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+ Execution get_execution() const override;
+
+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..e803ce3d
--- /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..de4bfe57
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+ 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..e21fad4d
--- /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..aca5fbce
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+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..ad412beb
--- /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..9598010c
--- /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) override;
+ void execute(RunContext& context) override {}
+ void post_process() override;
+
+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..be943533
--- /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..076b67dd
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+
+ Execution get_execution() const override;
+
+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..1890dc5f
--- /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..510fcfce
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+ void undo(Interface& target) override;
+
+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..fa36d739
--- /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..4fe42659
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+
+ 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..0c307e85
--- /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..f8469960
--- /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) override;
+ void execute(RunContext& context) override;
+ void post_process() override;
+
+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..71d1c8c5
--- /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 {
+ void load(ingen::World* world) override {
+ 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..51888972
--- /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) override {
+ 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..04cc5a33
--- /dev/null
+++ b/src/server/ingen_lv2.cpp
@@ -0,0 +1,848 @@
+/*
+ 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/atom/util.h"
+#include "lv2/buf-size/buf-size.h"
+#include "lv2/log/log.h"
+#include "lv2/log/logger.h"
+#include "lv2/options/options.h"
+#include "lv2/state/state.h"
+#include "lv2/urid/urid.h"
+#include "lv2/core/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)
+ {}
+
+ bool dynamic_ports() const override { 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;
+ }
+
+ void deactivate() override {
+ _engine.quit();
+ _main_sem.post();
+ }
+
+ virtual void set_root_graph(GraphImpl* graph) { _root_graph = graph; }
+ virtual GraphImpl* root_graph() { return _root_graph; }
+
+ EnginePort* get_port(const Raul::Path& path) override {
+ for (auto& p : _ports) {
+ if (p->graph_port()->path() == path) {
+ return p;
+ }
+ }
+
+ return nullptr;
+ }
+
+ /** Add a port. Called only during init or restore. */
+ void add_port(RunContext& context, EnginePort* port) override {
+ 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. */
+ void remove_port(RunContext& context, EnginePort* port) override {
+ const uint32_t index = port->graph_port()->index();
+ _ports[index] = nullptr;
+ }
+
+ /** Unused since LV2 has no dynamic ports. */
+ void register_port(EnginePort& port) override {}
+
+ /** Unused since LV2 has no dynamic ports. */
+ void unregister_port(EnginePort& port) override {}
+
+ /** Unused since LV2 has no dynamic ports. */
+ void rename_port(const Raul::Path& old_path,
+ const Raul::Path& new_path) override {}
+
+ /** Unused since LV2 has no dynamic ports. */
+ void port_property(const Raul::Path& path,
+ const URI& uri,
+ const Atom& value) override {}
+
+ EnginePort* create_port(DuplexPort* graph_port) override {
+ graph_port->set_is_driver_port(*_engine.buffer_factory());
+ return new EnginePort(graph_port);
+ }
+
+ void append_time_events(RunContext& context, Buffer& buffer) override {
+ 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));
+ }
+ }
+ }
+ }
+
+ int real_time_priority() override { 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) override {
+ // 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();
+ }
+ }
+
+ SampleCount block_length() const override { return _block_length; }
+ size_t seq_size() const override { return _seq_size; }
+ SampleCount sample_rate() const override { return _sample_rate; }
+ SampleCount frame_time() const override { 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..6d270443
--- /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) override {
+ 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..5c7ad147
--- /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..0e8fadce
--- /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) override;
+
+ void run(RunContext& context) override;
+
+ 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..4b5735d7
--- /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/atom/util.h"
+#include "lv2/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..d138e690
--- /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) override;
+
+ bool control(RunContext& context, uint8_t control_num, uint8_t val, FrameTime time);
+
+ void learn() override { _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..9d39f345
--- /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/atom/util.h"
+#include "lv2/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..fb935179
--- /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) override;
+ bool apply_poly(RunContext& context, uint32_t poly) override;
+
+ void run(RunContext& context) override;
+
+ 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..c35aa02b
--- /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/atom/util.h"
+#include "lv2/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..958cd239
--- /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) override;
+
+ 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..793e508d
--- /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/atom/util.h"
+#include "lv2/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..98d50f2c
--- /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) override;
+
+ 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() override { _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..065e3efc
--- /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/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..7df76595
--- /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..07a7f350
--- /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')