diff options
-rw-r--r-- | .gitattributes | 1 | ||||
-rw-r--r-- | AUTHORS | 5 | ||||
-rw-r--r-- | COPYING | 686 | ||||
-rw-r--r-- | INSTALL | 59 | ||||
-rw-r--r-- | README | 9 | ||||
-rw-r--r-- | THANKS | 13 | ||||
-rw-r--r-- | bundles/MonoEffect.ingen/MonoEffect.ttl | 76 | ||||
-rw-r--r-- | bundles/MonoEffect.ingen/manifest.ttl | 16 | ||||
-rw-r--r-- | bundles/MonoInstrument.ingen/MonoInstrument.ttl | 153 | ||||
-rw-r--r-- | bundles/MonoInstrument.ingen/manifest.ttl | 16 | ||||
-rw-r--r-- | bundles/StereoEffect.ingen/StereoEffect.ttl | 103 | ||||
-rw-r--r-- | bundles/StereoEffect.ingen/manifest.ttl | 16 | ||||
-rw-r--r-- | bundles/StereoInstrument.ingen/StereoInstrument.ttl | 165 | ||||
-rw-r--r-- | bundles/StereoInstrument.ingen/manifest.ttl | 16 | ||||
-rw-r--r-- | bundles/ingen.lv2/errors.ttl | 171 | ||||
-rw-r--r-- | bundles/ingen.lv2/ingen.ttl | 284 | ||||
-rw-r--r-- | bundles/ingen.lv2/internals.ttl | 33 | ||||
-rw-r--r-- | bundles/ingen.lv2/manifest.ttl | 43 | ||||
-rw-r--r-- | doc/ingen.1 | 85 | ||||
-rw-r--r-- | doc/reference.doxygen.in | 2418 | ||||
-rw-r--r-- | doc/style.css | 1145 | ||||
-rw-r--r-- | icons/128x128/ingen.png | bin | 0 -> 10239 bytes | |||
-rw-r--r-- | icons/16x16/ingen.png | bin | 0 -> 617 bytes | |||
-rw-r--r-- | icons/22x22/ingen.png | bin | 0 -> 1198 bytes | |||
-rw-r--r-- | icons/24x24/ingen.png | bin | 0 -> 1461 bytes | |||
-rw-r--r-- | icons/256x256/ingen.png | bin | 0 -> 24915 bytes | |||
-rw-r--r-- | icons/32x32/ingen.png | bin | 0 -> 1892 bytes | |||
-rw-r--r-- | icons/48x48/ingen.png | bin | 0 -> 2752 bytes | |||
-rw-r--r-- | icons/64x64/ingen.png | bin | 0 -> 4207 bytes | |||
-rw-r--r-- | icons/ingen_icons.svg | 7987 | ||||
-rw-r--r-- | icons/scalable/ingen.svg | 611 | ||||
-rw-r--r-- | ingen.ttl | 35 | ||||
-rw-r--r-- | ingen/Arc.hpp | 40 | ||||
-rw-r--r-- | ingen/Atom.hpp | 178 | ||||
-rw-r--r-- | ingen/AtomForgeSink.hpp | 102 | ||||
-rw-r--r-- | ingen/AtomReader.hpp | 76 | ||||
-rw-r--r-- | ingen/AtomSink.hpp | 47 | ||||
-rw-r--r-- | ingen/AtomWriter.hpp | 86 | ||||
-rw-r--r-- | ingen/ClashAvoider.hpp | 55 | ||||
-rw-r--r-- | ingen/Clock.hpp | 63 | ||||
-rw-r--r-- | ingen/ColorContext.hpp | 37 | ||||
-rw-r--r-- | ingen/Configuration.hpp | 160 | ||||
-rw-r--r-- | ingen/DataAccess.hpp | 68 | ||||
-rw-r--r-- | ingen/EngineBase.hpp | 145 | ||||
-rw-r--r-- | ingen/FilePath.hpp | 123 | ||||
-rw-r--r-- | ingen/Forge.hpp | 86 | ||||
-rw-r--r-- | ingen/InstanceAccess.hpp | 56 | ||||
-rw-r--r-- | ingen/Interface.hpp | 149 | ||||
-rw-r--r-- | ingen/LV2Features.hpp | 95 | ||||
-rw-r--r-- | ingen/Library.hpp | 48 | ||||
-rw-r--r-- | ingen/Log.hpp | 88 | ||||
-rw-r--r-- | ingen/Message.hpp | 158 | ||||
-rw-r--r-- | ingen/Module.hpp | 64 | ||||
-rw-r--r-- | ingen/Node.hpp | 106 | ||||
-rw-r--r-- | ingen/Parser.hpp | 99 | ||||
-rw-r--r-- | ingen/Properties.hpp | 90 | ||||
-rw-r--r-- | ingen/QueuedInterface.hpp | 66 | ||||
-rw-r--r-- | ingen/Resource.hpp | 205 | ||||
-rw-r--r-- | ingen/Serialiser.hpp | 103 | ||||
-rw-r--r-- | ingen/SocketReader.hpp | 78 | ||||
-rw-r--r-- | ingen/SocketWriter.hpp | 57 | ||||
-rw-r--r-- | ingen/Status.hpp | 92 | ||||
-rw-r--r-- | ingen/Store.hpp | 89 | ||||
-rw-r--r-- | ingen/StreamWriter.hpp | 52 | ||||
-rw-r--r-- | ingen/Tee.hpp | 63 | ||||
-rw-r--r-- | ingen/TurtleWriter.hpp | 68 | ||||
-rw-r--r-- | ingen/URI.hpp | 160 | ||||
-rw-r--r-- | ingen/URIMap.hpp | 100 | ||||
-rw-r--r-- | ingen/URIs.hpp | 234 | ||||
-rw-r--r-- | ingen/World.hpp | 150 | ||||
-rw-r--r-- | ingen/client/ArcModel.hpp | 68 | ||||
-rw-r--r-- | ingen/client/BlockModel.hpp | 118 | ||||
-rw-r--r-- | ingen/client/ClientStore.hpp | 127 | ||||
-rw-r--r-- | ingen/client/GraphModel.hpp | 74 | ||||
-rw-r--r-- | ingen/client/ObjectModel.hpp | 103 | ||||
-rw-r--r-- | ingen/client/PluginModel.hpp | 128 | ||||
-rw-r--r-- | ingen/client/PluginUI.hpp | 111 | ||||
-rw-r--r-- | ingen/client/PortModel.hpp | 97 | ||||
-rw-r--r-- | ingen/client/SigClientInterface.hpp | 64 | ||||
-rw-r--r-- | ingen/client/SocketClient.hpp | 80 | ||||
-rw-r--r-- | ingen/client/signal.hpp | 31 | ||||
-rw-r--r-- | ingen/filesystem.hpp | 86 | ||||
-rw-r--r-- | ingen/ingen.h | 75 | ||||
-rw-r--r-- | ingen/paths.hpp | 55 | ||||
-rw-r--r-- | ingen/runtime_paths.hpp | 42 | ||||
-rw-r--r-- | ingen/types.hpp | 62 | ||||
-rw-r--r-- | scripts/ingen.py | 308 | ||||
-rwxr-xr-x | scripts/ingenams | 283 | ||||
-rwxr-xr-x | scripts/ingenish | 121 | ||||
-rw-r--r-- | src/AtomReader.cpp | 384 | ||||
-rw-r--r-- | src/AtomWriter.cpp | 652 | ||||
-rw-r--r-- | src/ClashAvoider.cpp | 136 | ||||
-rw-r--r-- | src/ColorContext.cpp | 44 | ||||
-rw-r--r-- | src/Configuration.cpp | 386 | ||||
-rw-r--r-- | src/FilePath.cpp | 242 | ||||
-rw-r--r-- | src/Forge.cpp | 69 | ||||
-rw-r--r-- | src/LV2Features.cpp | 83 | ||||
-rw-r--r-- | src/Library.cpp | 56 | ||||
-rw-r--r-- | src/Log.cpp | 162 | ||||
-rw-r--r-- | src/Parser.cpp | 713 | ||||
-rw-r--r-- | src/Resource.cpp | 234 | ||||
-rw-r--r-- | src/Serialiser.cpp | 583 | ||||
-rw-r--r-- | src/SocketReader.cpp | 199 | ||||
-rw-r--r-- | src/SocketWriter.cpp | 58 | ||||
-rw-r--r-- | src/Store.cpp | 143 | ||||
-rw-r--r-- | src/StreamWriter.cpp | 39 | ||||
-rw-r--r-- | src/TurtleWriter.cpp | 101 | ||||
-rw-r--r-- | src/URI.cpp | 113 | ||||
-rw-r--r-- | src/URIMap.cpp | 123 | ||||
-rw-r--r-- | src/URIs.cpp | 204 | ||||
-rw-r--r-- | src/World.cpp | 355 | ||||
-rw-r--r-- | src/client/BlockModel.cpp | 285 | ||||
-rw-r--r-- | src/client/ClientStore.cpp | 487 | ||||
-rw-r--r-- | src/client/GraphModel.cpp | 176 | ||||
-rw-r--r-- | src/client/ObjectModel.cpp | 108 | ||||
-rw-r--r-- | src/client/PluginModel.cpp | 360 | ||||
-rw-r--r-- | src/client/PluginUI.cpp | 336 | ||||
-rw-r--r-- | src/client/PortModel.cpp | 78 | ||||
-rw-r--r-- | src/client/ingen_client.cpp | 34 | ||||
-rw-r--r-- | src/client/wscript | 23 | ||||
-rw-r--r-- | src/gui/App.cpp | 499 | ||||
-rw-r--r-- | src/gui/App.hpp | 196 | ||||
-rw-r--r-- | src/gui/Arc.cpp | 44 | ||||
-rw-r--r-- | src/gui/Arc.hpp | 52 | ||||
-rw-r--r-- | src/gui/BreadCrumbs.cpp | 229 | ||||
-rw-r--r-- | src/gui/BreadCrumbs.hpp | 119 | ||||
-rw-r--r-- | src/gui/ConnectWindow.cpp | 572 | ||||
-rw-r--r-- | src/gui/ConnectWindow.hpp | 116 | ||||
-rw-r--r-- | src/gui/GraphBox.cpp | 922 | ||||
-rw-r--r-- | src/gui/GraphBox.hpp | 213 | ||||
-rw-r--r-- | src/gui/GraphCanvas.cpp | 898 | ||||
-rw-r--r-- | src/gui/GraphCanvas.hpp | 159 | ||||
-rw-r--r-- | src/gui/GraphPortModule.cpp | 166 | ||||
-rw-r--r-- | src/gui/GraphPortModule.hpp | 79 | ||||
-rw-r--r-- | src/gui/GraphTreeWindow.cpp | 235 | ||||
-rw-r--r-- | src/gui/GraphTreeWindow.hpp | 123 | ||||
-rw-r--r-- | src/gui/GraphView.cpp | 154 | ||||
-rw-r--r-- | src/gui/GraphView.hpp | 98 | ||||
-rw-r--r-- | src/gui/GraphWindow.cpp | 85 | ||||
-rw-r--r-- | src/gui/GraphWindow.hpp | 80 | ||||
-rw-r--r-- | src/gui/LoadGraphWindow.cpp | 257 | ||||
-rw-r--r-- | src/gui/LoadGraphWindow.hpp | 95 | ||||
-rw-r--r-- | src/gui/LoadPluginWindow.cpp | 515 | ||||
-rw-r--r-- | src/gui/LoadPluginWindow.hpp | 162 | ||||
-rw-r--r-- | src/gui/MessagesWindow.cpp | 141 | ||||
-rw-r--r-- | src/gui/MessagesWindow.hpp | 70 | ||||
-rw-r--r-- | src/gui/NewSubgraphWindow.cpp | 119 | ||||
-rw-r--r-- | src/gui/NewSubgraphWindow.hpp | 72 | ||||
-rw-r--r-- | src/gui/NodeMenu.cpp | 253 | ||||
-rw-r--r-- | src/gui/NodeMenu.hpp | 75 | ||||
-rw-r--r-- | src/gui/NodeModule.cpp | 518 | ||||
-rw-r--r-- | src/gui/NodeModule.hpp | 104 | ||||
-rw-r--r-- | src/gui/ObjectMenu.cpp | 145 | ||||
-rw-r--r-- | src/gui/ObjectMenu.hpp | 77 | ||||
-rw-r--r-- | src/gui/PluginMenu.cpp | 176 | ||||
-rw-r--r-- | src/gui/PluginMenu.hpp | 80 | ||||
-rw-r--r-- | src/gui/Port.cpp | 534 | ||||
-rw-r--r-- | src/gui/Port.hpp | 102 | ||||
-rw-r--r-- | src/gui/PortMenu.cpp | 174 | ||||
-rw-r--r-- | src/gui/PortMenu.hpp | 66 | ||||
-rw-r--r-- | src/gui/PropertiesWindow.cpp | 591 | ||||
-rw-r--r-- | src/gui/PropertiesWindow.hpp | 129 | ||||
-rw-r--r-- | src/gui/RDFS.cpp | 259 | ||||
-rw-r--r-- | src/gui/RDFS.hpp | 80 | ||||
-rw-r--r-- | src/gui/RenameWindow.cpp | 137 | ||||
-rw-r--r-- | src/gui/RenameWindow.hpp | 64 | ||||
-rw-r--r-- | src/gui/Style.cpp | 106 | ||||
-rw-r--r-- | src/gui/Style.hpp | 56 | ||||
-rw-r--r-- | src/gui/SubgraphModule.cpp | 102 | ||||
-rw-r--r-- | src/gui/SubgraphModule.hpp | 64 | ||||
-rw-r--r-- | src/gui/ThreadedLoader.cpp | 148 | ||||
-rw-r--r-- | src/gui/ThreadedLoader.hpp | 96 | ||||
-rw-r--r-- | src/gui/URIEntry.cpp | 192 | ||||
-rw-r--r-- | src/gui/URIEntry.hpp | 68 | ||||
-rw-r--r-- | src/gui/WidgetFactory.cpp | 80 | ||||
-rw-r--r-- | src/gui/WidgetFactory.hpp | 58 | ||||
-rw-r--r-- | src/gui/Window.hpp | 78 | ||||
-rw-r--r-- | src/gui/WindowFactory.cpp | 302 | ||||
-rw-r--r-- | src/gui/WindowFactory.hpp | 99 | ||||
-rw-r--r-- | src/gui/ingen_gui.cpp | 67 | ||||
-rw-r--r-- | src/gui/ingen_gui.gladep | 9 | ||||
-rw-r--r-- | src/gui/ingen_gui.ui | 3049 | ||||
-rw-r--r-- | src/gui/ingen_gui_lv2.cpp | 209 | ||||
-rw-r--r-- | src/gui/ingen_style.rc | 155 | ||||
-rw-r--r-- | src/gui/rgba.hpp | 58 | ||||
-rw-r--r-- | src/gui/wscript | 113 | ||||
-rw-r--r-- | src/ingen/ingen.cpp | 263 | ||||
-rw-r--r-- | src/ingen/ingen.desktop | 9 | ||||
-rw-r--r-- | src/ingen/ingen.grind | 5 | ||||
-rw-r--r-- | src/runtime_paths.cpp | 146 | ||||
-rw-r--r-- | src/server/ArcImpl.cpp | 114 | ||||
-rw-r--r-- | src/server/ArcImpl.hpp | 84 | ||||
-rw-r--r-- | src/server/BlockFactory.cpp | 229 | ||||
-rw-r--r-- | src/server/BlockFactory.hpp | 67 | ||||
-rw-r--r-- | src/server/BlockImpl.cpp | 303 | ||||
-rw-r--r-- | src/server/BlockImpl.hpp | 207 | ||||
-rw-r--r-- | src/server/Broadcaster.cpp | 97 | ||||
-rw-r--r-- | src/server/Broadcaster.hpp | 118 | ||||
-rw-r--r-- | src/server/Buffer.cpp | 468 | ||||
-rw-r--r-- | src/server/Buffer.hpp | 244 | ||||
-rw-r--r-- | src/server/BufferFactory.cpp | 190 | ||||
-rw-r--r-- | src/server/BufferFactory.hpp | 118 | ||||
-rw-r--r-- | src/server/BufferRef.hpp | 38 | ||||
-rw-r--r-- | src/server/ClientUpdate.cpp | 155 | ||||
-rw-r--r-- | src/server/ClientUpdate.hpp | 80 | ||||
-rw-r--r-- | src/server/CompiledGraph.cpp | 274 | ||||
-rw-r--r-- | src/server/CompiledGraph.hpp | 84 | ||||
-rw-r--r-- | src/server/ControlBindings.cpp | 425 | ||||
-rw-r--r-- | src/server/ControlBindings.hpp | 148 | ||||
-rw-r--r-- | src/server/DirectDriver.hpp | 108 | ||||
-rw-r--r-- | src/server/Driver.hpp | 110 | ||||
-rw-r--r-- | src/server/DuplexPort.cpp | 236 | ||||
-rw-r--r-- | src/server/DuplexPort.hpp | 98 | ||||
-rw-r--r-- | src/server/Engine.cpp | 526 | ||||
-rw-r--r-- | src/server/Engine.hpp | 221 | ||||
-rw-r--r-- | src/server/EnginePort.hpp | 66 | ||||
-rw-r--r-- | src/server/Event.hpp | 163 | ||||
-rw-r--r-- | src/server/EventWriter.cpp | 147 | ||||
-rw-r--r-- | src/server/EventWriter.hpp | 86 | ||||
-rw-r--r-- | src/server/FrameTimer.hpp | 110 | ||||
-rw-r--r-- | src/server/GraphImpl.cpp | 379 | ||||
-rw-r--r-- | src/server/GraphImpl.hpp | 200 | ||||
-rw-r--r-- | src/server/GraphPlugin.hpp | 63 | ||||
-rw-r--r-- | src/server/InputPort.cpp | 261 | ||||
-rw-r--r-- | src/server/InputPort.hpp | 128 | ||||
-rw-r--r-- | src/server/InternalBlock.cpp | 73 | ||||
-rw-r--r-- | src/server/InternalBlock.hpp | 48 | ||||
-rw-r--r-- | src/server/InternalPlugin.cpp | 67 | ||||
-rw-r--r-- | src/server/InternalPlugin.hpp | 57 | ||||
-rw-r--r-- | src/server/JackDriver.cpp | 584 | ||||
-rw-r--r-- | src/server/JackDriver.hpp | 169 | ||||
-rw-r--r-- | src/server/LV2Block.cpp | 742 | ||||
-rw-r--r-- | src/server/LV2Block.hpp | 152 | ||||
-rw-r--r-- | src/server/LV2Options.hpp | 71 | ||||
-rw-r--r-- | src/server/LV2Plugin.cpp | 143 | ||||
-rw-r--r-- | src/server/LV2Plugin.hpp | 72 | ||||
-rw-r--r-- | src/server/LV2ResizeFeature.hpp | 65 | ||||
-rw-r--r-- | src/server/Load.hpp | 57 | ||||
-rw-r--r-- | src/server/NodeImpl.cpp | 50 | ||||
-rw-r--r-- | src/server/NodeImpl.hpp | 109 | ||||
-rw-r--r-- | src/server/OutputPort.hpp | 51 | ||||
-rw-r--r-- | src/server/PluginImpl.hpp | 96 | ||||
-rw-r--r-- | src/server/PortAudioDriver.cpp | 297 | ||||
-rw-r--r-- | src/server/PortAudioDriver.hpp | 132 | ||||
-rw-r--r-- | src/server/PortImpl.cpp | 569 | ||||
-rw-r--r-- | src/server/PortImpl.hpp | 312 | ||||
-rw-r--r-- | src/server/PortType.hpp | 91 | ||||
-rw-r--r-- | src/server/PostProcessor.cpp | 114 | ||||
-rw-r--r-- | src/server/PostProcessor.hpp | 74 | ||||
-rw-r--r-- | src/server/PreProcessContext.hpp | 84 | ||||
-rw-r--r-- | src/server/PreProcessor.cpp | 248 | ||||
-rw-r--r-- | src/server/PreProcessor.hpp | 87 | ||||
-rw-r--r-- | src/server/RunContext.cpp | 195 | ||||
-rw-r--r-- | src/server/RunContext.hpp | 161 | ||||
-rw-r--r-- | src/server/SocketListener.cpp | 190 | ||||
-rw-r--r-- | src/server/SocketListener.hpp | 41 | ||||
-rw-r--r-- | src/server/SocketServer.hpp | 80 | ||||
-rw-r--r-- | src/server/Task.cpp | 158 | ||||
-rw-r--r-- | src/server/Task.hpp | 120 | ||||
-rw-r--r-- | src/server/ThreadManager.hpp | 68 | ||||
-rw-r--r-- | src/server/UndoStack.cpp | 253 | ||||
-rw-r--r-- | src/server/UndoStack.hpp | 107 | ||||
-rw-r--r-- | src/server/Worker.cpp | 163 | ||||
-rw-r--r-- | src/server/Worker.hpp | 76 | ||||
-rw-r--r-- | src/server/events.hpp | 35 | ||||
-rw-r--r-- | src/server/events/Connect.cpp | 188 | ||||
-rw-r--r-- | src/server/events/Connect.hpp | 74 | ||||
-rw-r--r-- | src/server/events/Copy.cpp | 216 | ||||
-rw-r--r-- | src/server/events/Copy.hpp | 68 | ||||
-rw-r--r-- | src/server/events/CreateBlock.cpp | 180 | ||||
-rw-r--r-- | src/server/events/CreateBlock.hpp | 66 | ||||
-rw-r--r-- | src/server/events/CreateGraph.cpp | 236 | ||||
-rw-r--r-- | src/server/events/CreateGraph.hpp | 74 | ||||
-rw-r--r-- | src/server/events/CreatePort.cpp | 219 | ||||
-rw-r--r-- | src/server/events/CreatePort.hpp | 82 | ||||
-rw-r--r-- | src/server/events/Delete.cpp | 216 | ||||
-rw-r--r-- | src/server/events/Delete.hpp | 86 | ||||
-rw-r--r-- | src/server/events/Delta.cpp | 670 | ||||
-rw-r--r-- | src/server/events/Delta.hpp | 133 | ||||
-rw-r--r-- | src/server/events/Disconnect.cpp | 224 | ||||
-rw-r--r-- | src/server/events/Disconnect.hpp | 87 | ||||
-rw-r--r-- | src/server/events/DisconnectAll.cpp | 176 | ||||
-rw-r--r-- | src/server/events/DisconnectAll.hpp | 78 | ||||
-rw-r--r-- | src/server/events/Get.cpp | 111 | ||||
-rw-r--r-- | src/server/events/Get.hpp | 65 | ||||
-rw-r--r-- | src/server/events/Mark.cpp | 112 | ||||
-rw-r--r-- | src/server/events/Mark.hpp | 69 | ||||
-rw-r--r-- | src/server/events/Move.cpp | 91 | ||||
-rw-r--r-- | src/server/events/Move.hpp | 57 | ||||
-rw-r--r-- | src/server/events/SetPortValue.cpp | 139 | ||||
-rw-r--r-- | src/server/events/SetPortValue.hpp | 71 | ||||
-rw-r--r-- | src/server/events/Undo.cpp | 85 | ||||
-rw-r--r-- | src/server/events/Undo.hpp | 58 | ||||
-rw-r--r-- | src/server/ingen_engine.cpp | 44 | ||||
-rw-r--r-- | src/server/ingen_jack.cpp | 58 | ||||
-rw-r--r-- | src/server/ingen_lv2.cpp | 850 | ||||
-rw-r--r-- | src/server/ingen_portaudio.cpp | 54 | ||||
-rw-r--r-- | src/server/internals/BlockDelay.cpp | 89 | ||||
-rw-r--r-- | src/server/internals/BlockDelay.hpp | 62 | ||||
-rw-r--r-- | src/server/internals/Controller.cpp | 174 | ||||
-rw-r--r-- | src/server/internals/Controller.hpp | 71 | ||||
-rw-r--r-- | src/server/internals/Note.cpp | 420 | ||||
-rw-r--r-- | src/server/internals/Note.hpp | 109 | ||||
-rw-r--r-- | src/server/internals/Time.cpp | 78 | ||||
-rw-r--r-- | src/server/internals/Time.hpp | 59 | ||||
-rw-r--r-- | src/server/internals/Trigger.cpp | 187 | ||||
-rw-r--r-- | src/server/internals/Trigger.hpp | 75 | ||||
-rw-r--r-- | src/server/jackey.h | 72 | ||||
-rw-r--r-- | src/server/mix.cpp | 112 | ||||
-rw-r--r-- | src/server/mix.hpp | 40 | ||||
-rw-r--r-- | src/server/types.hpp | 27 | ||||
-rw-r--r-- | src/server/util.hpp | 63 | ||||
-rw-r--r-- | src/server/wscript | 104 | ||||
-rw-r--r-- | src/wscript | 46 | ||||
-rw-r--r-- | tests/TestClient.hpp | 54 | ||||
-rw-r--r-- | tests/connect_disconnect_node_node.ttl | 36 | ||||
-rw-r--r-- | tests/connect_disconnect_node_patch.ttl | 105 | ||||
-rw-r--r-- | tests/connect_disconnect_patch_patch.ttl | 36 | ||||
-rw-r--r-- | tests/copy_node.ttl | 16 | ||||
-rw-r--r-- | tests/create_delete_node.ttl | 27 | ||||
-rw-r--r-- | tests/create_delete_patch.ttl | 14 | ||||
-rw-r--r-- | tests/create_delete_poly_patch.ttl | 15 | ||||
-rw-r--r-- | tests/create_delete_port.ttl | 53 | ||||
-rw-r--r-- | tests/disconnect_all_node.ttl | 45 | ||||
-rw-r--r-- | tests/disconnect_all_port.ttl | 32 | ||||
-rw-r--r-- | tests/duplicate_node.ttl | 19 | ||||
-rw-r--r-- | tests/empty.ingen/main.ttl | 52 | ||||
-rw-r--r-- | tests/empty.ingen/manifest.ttl | 16 | ||||
-rw-r--r-- | tests/enable_graph.ttl | 15 | ||||
-rw-r--r-- | tests/get_engine.ttl | 7 | ||||
-rw-r--r-- | tests/get_node.ttl | 15 | ||||
-rw-r--r-- | tests/get_patch.ttl | 39 | ||||
-rw-r--r-- | tests/get_plugin.ttl | 7 | ||||
-rw-r--r-- | tests/get_plugins.ttl | 7 | ||||
-rw-r--r-- | tests/get_port.ttl | 15 | ||||
-rw-r--r-- | tests/ingen_bench.cpp | 140 | ||||
-rw-r--r-- | tests/ingen_test.cpp | 223 | ||||
-rw-r--r-- | tests/load_graph.ttl | 8 | ||||
-rw-r--r-- | tests/move_node.ttl | 16 | ||||
-rw-r--r-- | tests/move_port.ttl | 16 | ||||
-rw-r--r-- | tests/move_root_port.ttl | 20 | ||||
-rw-r--r-- | tests/poly.ttl | 25 | ||||
-rw-r--r-- | tests/put_audio_in.ttl | 10 | ||||
-rw-r--r-- | tests/save_graph.ttl | 8 | ||||
-rw-r--r-- | tests/set_graph_poly.ttl | 17 | ||||
-rw-r--r-- | tests/set_patch_port_value.ttl | 17 | ||||
-rw-r--r-- | tests/test_utils.hpp | 40 | ||||
-rw-r--r-- | tests/tst_FilePath.cpp | 103 | ||||
-rwxr-xr-x | waf | 171 | ||||
-rw-r--r-- | waflib/.gitignore (renamed from .gitignore) | 0 | ||||
-rw-r--r-- | waflib/Build.py (renamed from Build.py) | 0 | ||||
-rw-r--r-- | waflib/COPYING | 25 | ||||
-rw-r--r-- | waflib/ConfigSet.py (renamed from ConfigSet.py) | 0 | ||||
-rw-r--r-- | waflib/Configure.py (renamed from Configure.py) | 0 | ||||
-rw-r--r-- | waflib/Context.py (renamed from Context.py) | 0 | ||||
-rw-r--r-- | waflib/Errors.py (renamed from Errors.py) | 0 | ||||
-rw-r--r-- | waflib/Logs.py (renamed from Logs.py) | 0 | ||||
-rw-r--r-- | waflib/Node.py (renamed from Node.py) | 0 | ||||
-rw-r--r-- | waflib/Options.py (renamed from Options.py) | 0 | ||||
-rw-r--r-- | waflib/README.md (renamed from README.md) | 0 | ||||
-rw-r--r-- | waflib/Runner.py (renamed from Runner.py) | 0 | ||||
-rw-r--r-- | waflib/Scripting.py (renamed from Scripting.py) | 0 | ||||
-rw-r--r-- | waflib/Task.py (renamed from Task.py) | 0 | ||||
-rw-r--r-- | waflib/TaskGen.py (renamed from TaskGen.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/__init__.py (renamed from Tools/__init__.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/ar.py (renamed from Tools/ar.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/asm.py (renamed from Tools/asm.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/bison.py (renamed from Tools/bison.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/c.py (renamed from Tools/c.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/c_aliases.py (renamed from Tools/c_aliases.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/c_config.py (renamed from Tools/c_config.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/c_osx.py (renamed from Tools/c_osx.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/c_preproc.py (renamed from Tools/c_preproc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/c_tests.py (renamed from Tools/c_tests.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/ccroot.py (renamed from Tools/ccroot.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/clang.py (renamed from Tools/clang.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/clangxx.py (renamed from Tools/clangxx.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/compiler_c.py (renamed from Tools/compiler_c.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/compiler_cxx.py (renamed from Tools/compiler_cxx.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/compiler_d.py (renamed from Tools/compiler_d.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/compiler_fc.py (renamed from Tools/compiler_fc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/cs.py (renamed from Tools/cs.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/cxx.py (renamed from Tools/cxx.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/d.py (renamed from Tools/d.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/d_config.py (renamed from Tools/d_config.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/d_scan.py (renamed from Tools/d_scan.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/dbus.py (renamed from Tools/dbus.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/dmd.py (renamed from Tools/dmd.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/errcheck.py (renamed from Tools/errcheck.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/fc.py (renamed from Tools/fc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/fc_config.py (renamed from Tools/fc_config.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/fc_scan.py (renamed from Tools/fc_scan.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/flex.py (renamed from Tools/flex.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/g95.py (renamed from Tools/g95.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/gas.py (renamed from Tools/gas.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/gcc.py (renamed from Tools/gcc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/gdc.py (renamed from Tools/gdc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/gfortran.py (renamed from Tools/gfortran.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/glib2.py (renamed from Tools/glib2.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/gnu_dirs.py (renamed from Tools/gnu_dirs.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/gxx.py (renamed from Tools/gxx.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/icc.py (renamed from Tools/icc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/icpc.py (renamed from Tools/icpc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/ifort.py (renamed from Tools/ifort.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/intltool.py (renamed from Tools/intltool.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/irixcc.py (renamed from Tools/irixcc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/javaw.py (renamed from Tools/javaw.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/ldc2.py (renamed from Tools/ldc2.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/lua.py (renamed from Tools/lua.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/md5_tstamp.py (renamed from Tools/md5_tstamp.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/msvc.py (renamed from Tools/msvc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/nasm.py (renamed from Tools/nasm.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/nobuild.py (renamed from Tools/nobuild.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/perl.py (renamed from Tools/perl.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/python.py (renamed from Tools/python.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/qt5.py (renamed from Tools/qt5.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/ruby.py (renamed from Tools/ruby.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/suncc.py (renamed from Tools/suncc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/suncxx.py (renamed from Tools/suncxx.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/tex.py (renamed from Tools/tex.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/vala.py (renamed from Tools/vala.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/waf_unit_test.py (renamed from Tools/waf_unit_test.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/winres.py (renamed from Tools/winres.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/xlc.py (renamed from Tools/xlc.py) | 0 | ||||
-rw-r--r-- | waflib/Tools/xlcxx.py (renamed from Tools/xlcxx.py) | 0 | ||||
-rw-r--r-- | waflib/Utils.py (renamed from Utils.py) | 0 | ||||
-rw-r--r-- | waflib/__init__.py (renamed from __init__.py) | 0 | ||||
-rw-r--r-- | waflib/ansiterm.py (renamed from ansiterm.py) | 0 | ||||
-rw-r--r-- | waflib/extras/__init__.py (renamed from extras/__init__.py) | 0 | ||||
-rw-r--r-- | waflib/extras/autowaf.py (renamed from extras/autowaf.py) | 0 | ||||
-rw-r--r-- | waflib/extras/batched_cc.py (renamed from extras/batched_cc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/biber.py (renamed from extras/biber.py) | 0 | ||||
-rw-r--r-- | waflib/extras/bjam.py (renamed from extras/bjam.py) | 0 | ||||
-rw-r--r-- | waflib/extras/blender.py (renamed from extras/blender.py) | 0 | ||||
-rw-r--r-- | waflib/extras/boo.py (renamed from extras/boo.py) | 0 | ||||
-rw-r--r-- | waflib/extras/boost.py (renamed from extras/boost.py) | 0 | ||||
-rw-r--r-- | waflib/extras/build_file_tracker.py (renamed from extras/build_file_tracker.py) | 0 | ||||
-rw-r--r-- | waflib/extras/build_logs.py (renamed from extras/build_logs.py) | 0 | ||||
-rw-r--r-- | waflib/extras/buildcopy.py (renamed from extras/buildcopy.py) | 0 | ||||
-rw-r--r-- | waflib/extras/c_bgxlc.py (renamed from extras/c_bgxlc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/c_dumbpreproc.py (renamed from extras/c_dumbpreproc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/c_emscripten.py (renamed from extras/c_emscripten.py) | 0 | ||||
-rw-r--r-- | waflib/extras/c_nec.py (renamed from extras/c_nec.py) | 0 | ||||
-rw-r--r-- | waflib/extras/cabal.py (renamed from extras/cabal.py) | 0 | ||||
-rw-r--r-- | waflib/extras/cfg_altoptions.py (renamed from extras/cfg_altoptions.py) | 0 | ||||
-rw-r--r-- | waflib/extras/clang_compilation_database.py (renamed from extras/clang_compilation_database.py) | 0 | ||||
-rw-r--r-- | waflib/extras/codelite.py (renamed from extras/codelite.py) | 0 | ||||
-rw-r--r-- | waflib/extras/color_gcc.py (renamed from extras/color_gcc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/color_rvct.py (renamed from extras/color_rvct.py) | 0 | ||||
-rw-r--r-- | waflib/extras/compat15.py (renamed from extras/compat15.py) | 0 | ||||
-rw-r--r-- | waflib/extras/cppcheck.py (renamed from extras/cppcheck.py) | 0 | ||||
-rw-r--r-- | waflib/extras/cpplint.py (renamed from extras/cpplint.py) | 0 | ||||
-rw-r--r-- | waflib/extras/cross_gnu.py (renamed from extras/cross_gnu.py) | 0 | ||||
-rw-r--r-- | waflib/extras/cython.py (renamed from extras/cython.py) | 0 | ||||
-rw-r--r-- | waflib/extras/dcc.py (renamed from extras/dcc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/distnet.py (renamed from extras/distnet.py) | 0 | ||||
-rw-r--r-- | waflib/extras/doxygen.py (renamed from extras/doxygen.py) | 0 | ||||
-rw-r--r-- | waflib/extras/dpapi.py (renamed from extras/dpapi.py) | 0 | ||||
-rw-r--r-- | waflib/extras/eclipse.py (renamed from extras/eclipse.py) | 0 | ||||
-rw-r--r-- | waflib/extras/erlang.py (renamed from extras/erlang.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fast_partial.py (renamed from extras/fast_partial.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fc_bgxlf.py (renamed from extras/fc_bgxlf.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fc_cray.py (renamed from extras/fc_cray.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fc_nag.py (renamed from extras/fc_nag.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fc_nec.py (renamed from extras/fc_nec.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fc_open64.py (renamed from extras/fc_open64.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fc_pgfortran.py (renamed from extras/fc_pgfortran.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fc_solstudio.py (renamed from extras/fc_solstudio.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fc_xlf.py (renamed from extras/fc_xlf.py) | 0 | ||||
-rw-r--r-- | waflib/extras/file_to_object.py (renamed from extras/file_to_object.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fluid.py (renamed from extras/fluid.py) | 0 | ||||
-rw-r--r-- | waflib/extras/freeimage.py (renamed from extras/freeimage.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fsb.py (renamed from extras/fsb.py) | 0 | ||||
-rw-r--r-- | waflib/extras/fsc.py (renamed from extras/fsc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/gccdeps.py (renamed from extras/gccdeps.py) | 0 | ||||
-rw-r--r-- | waflib/extras/gdbus.py (renamed from extras/gdbus.py) | 0 | ||||
-rw-r--r-- | waflib/extras/gob2.py (renamed from extras/gob2.py) | 0 | ||||
-rw-r--r-- | waflib/extras/halide.py (renamed from extras/halide.py) | 0 | ||||
-rwxr-xr-x | waflib/extras/javatest.py (renamed from extras/javatest.py) | 0 | ||||
-rw-r--r-- | waflib/extras/kde4.py (renamed from extras/kde4.py) | 0 | ||||
-rw-r--r-- | waflib/extras/local_rpath.py (renamed from extras/local_rpath.py) | 0 | ||||
-rw-r--r-- | waflib/extras/lv2.py (renamed from extras/lv2.py) | 0 | ||||
-rw-r--r-- | waflib/extras/make.py (renamed from extras/make.py) | 0 | ||||
-rw-r--r-- | waflib/extras/midl.py (renamed from extras/midl.py) | 0 | ||||
-rw-r--r-- | waflib/extras/msvcdeps.py (renamed from extras/msvcdeps.py) | 0 | ||||
-rw-r--r-- | waflib/extras/msvs.py (renamed from extras/msvs.py) | 0 | ||||
-rw-r--r-- | waflib/extras/netcache_client.py (renamed from extras/netcache_client.py) | 0 | ||||
-rw-r--r-- | waflib/extras/objcopy.py (renamed from extras/objcopy.py) | 0 | ||||
-rw-r--r-- | waflib/extras/ocaml.py (renamed from extras/ocaml.py) | 0 | ||||
-rw-r--r-- | waflib/extras/package.py (renamed from extras/package.py) | 0 | ||||
-rw-r--r-- | waflib/extras/parallel_debug.py (renamed from extras/parallel_debug.py) | 0 | ||||
-rw-r--r-- | waflib/extras/pch.py (renamed from extras/pch.py) | 0 | ||||
-rw-r--r-- | waflib/extras/pep8.py (renamed from extras/pep8.py) | 0 | ||||
-rw-r--r-- | waflib/extras/pgicc.py (renamed from extras/pgicc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/pgicxx.py (renamed from extras/pgicxx.py) | 0 | ||||
-rw-r--r-- | waflib/extras/proc.py (renamed from extras/proc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/protoc.py (renamed from extras/protoc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/pyqt5.py (renamed from extras/pyqt5.py) | 0 | ||||
-rw-r--r-- | waflib/extras/pytest.py (renamed from extras/pytest.py) | 0 | ||||
-rw-r--r-- | waflib/extras/qnxnto.py (renamed from extras/qnxnto.py) | 0 | ||||
-rw-r--r-- | waflib/extras/qt4.py (renamed from extras/qt4.py) | 0 | ||||
-rw-r--r-- | waflib/extras/relocation.py (renamed from extras/relocation.py) | 0 | ||||
-rw-r--r-- | waflib/extras/remote.py (renamed from extras/remote.py) | 0 | ||||
-rw-r--r-- | waflib/extras/resx.py (renamed from extras/resx.py) | 0 | ||||
-rw-r--r-- | waflib/extras/review.py (renamed from extras/review.py) | 0 | ||||
-rw-r--r-- | waflib/extras/rst.py (renamed from extras/rst.py) | 0 | ||||
-rw-r--r-- | waflib/extras/run_do_script.py (renamed from extras/run_do_script.py) | 0 | ||||
-rw-r--r-- | waflib/extras/run_m_script.py (renamed from extras/run_m_script.py) | 0 | ||||
-rw-r--r-- | waflib/extras/run_py_script.py (renamed from extras/run_py_script.py) | 0 | ||||
-rw-r--r-- | waflib/extras/run_r_script.py (renamed from extras/run_r_script.py) | 0 | ||||
-rw-r--r-- | waflib/extras/sas.py (renamed from extras/sas.py) | 0 | ||||
-rw-r--r-- | waflib/extras/satellite_assembly.py (renamed from extras/satellite_assembly.py) | 0 | ||||
-rw-r--r-- | waflib/extras/scala.py (renamed from extras/scala.py) | 0 | ||||
-rw-r--r-- | waflib/extras/slow_qt4.py (renamed from extras/slow_qt4.py) | 0 | ||||
-rw-r--r-- | waflib/extras/softlink_libs.py (renamed from extras/softlink_libs.py) | 0 | ||||
-rw-r--r-- | waflib/extras/stale.py (renamed from extras/stale.py) | 0 | ||||
-rw-r--r-- | waflib/extras/stracedeps.py (renamed from extras/stracedeps.py) | 0 | ||||
-rw-r--r-- | waflib/extras/swig.py (renamed from extras/swig.py) | 0 | ||||
-rw-r--r-- | waflib/extras/syms.py (renamed from extras/syms.py) | 0 | ||||
-rw-r--r-- | waflib/extras/ticgt.py (renamed from extras/ticgt.py) | 0 | ||||
-rw-r--r-- | waflib/extras/unity.py (renamed from extras/unity.py) | 0 | ||||
-rw-r--r-- | waflib/extras/use_config.py (renamed from extras/use_config.py) | 0 | ||||
-rw-r--r-- | waflib/extras/valadoc.py (renamed from extras/valadoc.py) | 0 | ||||
-rw-r--r-- | waflib/extras/waf_xattr.py (renamed from extras/waf_xattr.py) | 0 | ||||
-rw-r--r-- | waflib/extras/why.py (renamed from extras/why.py) | 0 | ||||
-rw-r--r-- | waflib/extras/win32_opts.py (renamed from extras/win32_opts.py) | 0 | ||||
-rw-r--r-- | waflib/extras/wix.py (renamed from extras/wix.py) | 0 | ||||
-rw-r--r-- | waflib/extras/xcode6.py (renamed from extras/xcode6.py) | 0 | ||||
-rw-r--r-- | waflib/fixpy2.py (renamed from fixpy2.py) | 0 | ||||
-rwxr-xr-x | waflib/processor.py (renamed from processor.py) | 0 | ||||
-rwxr-xr-x | waflib/waf | 16 | ||||
-rw-r--r-- | wscript | 370 |
532 files changed, 63594 insertions, 34 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f063da37 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +waf binary
\ No newline at end of file diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..b5e95e99 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,5 @@ +Ingen was written by David Robillard <d@drobilla.net>. + +Discussion should be directed to the mailing list +<http://lists.drobilla.net/listinfo.cgi/ingen-drobilla.net> or #ingen on +irc.freenode.net. @@ -1,25 +1,661 @@ -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING -IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<http://www.gnu.org/licenses/>. diff --git a/INSTALL b/INSTALL new file mode 100644 index 00000000..623cddde --- /dev/null +++ b/INSTALL @@ -0,0 +1,59 @@ +Installation Instructions +========================= + +Basic Installation +------------------ + +Building this software requires only Python. To install with default options: + + ./waf configure + ./waf + ./waf install + +You may need to become root for the install stage, for example: + + sudo ./waf install + +Configuration Options +--------------------- + +All supported options can be viewed using the command: + + ./waf --help + +Most options only need to be passed during the configure stage, for example: + + ./waf configure --prefix=/usr + ./waf + ./waf install + +Compiler Configuration +---------------------- + +Several standard environment variables can be used to control how compilers are +invoked: + + * CC: Path to C compiler + * CFLAGS: C compiler options + * CXX: Path to C++ compiler + * CXXFLAGS: C++ compiler options + * CPPFLAGS: C preprocessor options + * LINKFLAGS: Linker options + +Installation Directories +------------------------ + +The --prefix option (or the PREFIX environment variable) can be used to change +the prefix which all files are installed under. There are also several options +allowing for more fine-tuned control, see the --help output for details. + +Packaging +--------- + +Everything can be installed to a specific root directory by passing a --destdir +option to the install stage (or setting the DESTDIR environment variable), +which adds a prefix to all install paths. For example: + + ./waf configure --prefix=/usr + ./waf + ./waf install --destdir=/tmp/package @@ -0,0 +1,9 @@ +Ingen +===== + +Ingen is a realtime modular synthesizer and/or effects processor for +Jack/LV2/etc. (i.e. GNU/Linux audio systems). + +For more information, see <http://drobilla.net/software/ingen>. + + -- David Robillard <d@drobilla.net> @@ -0,0 +1,13 @@ +Thanks to (in alphabetical order): + +Lachlan Davis (LFactor) +Paul Davis (las) +Krzysztof Foltman (kfoltman) +Steve Harris (swh) +Mario Lang (delYsid) +Lars Luthman (larsl) +Leonard Ritter (paniq) +Thorsten Wilms (thorwil) + +Everyone who has ever written a Free plugin. + diff --git a/bundles/MonoEffect.ingen/MonoEffect.ttl b/bundles/MonoEffect.ingen/MonoEffect.ttl new file mode 100644 index 00000000..45f55ad5 --- /dev/null +++ b/bundles/MonoEffect.ingen/MonoEffect.ttl @@ -0,0 +1,76 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<> + ingen:arc [ + ingen:head <audio_out> ; + ingen:tail <audio_in> + ] , [ + ingen:head <notify> ; + ingen:tail <control> + ] ; + ingen:polyphony 1 ; + <http://lv2plug.in/ns/extensions/ui#ui> ingen:GraphUIGtk2 ; + lv2:extensionData <http://lv2plug.in/ns/ext/state#interface> ; + lv2:port <control> , + <notify> , + <audio_in> , + <audio_out> ; + doap:name "Ingen Mono Effect Template" ; + a ingen:Graph , + lv2:Plugin . + +<control> + ingen:canvasX 9.5 ; + ingen:canvasY 112.0 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:index 0 ; + lv2:name "Control" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "control" ; + a atom:AtomPort , + lv2:InputPort . + +<notify> + ingen:canvasX 187.5 ; + ingen:canvasY 112.0 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:index 1 ; + lv2:name "Control" ; + lv2:symbol "notify" ; + a atom:AtomPort , + lv2:OutputPort . + +<audio_in> + ingen:canvasX 13.0 ; + ingen:canvasY 60.0 ; + ingen:polyphonic false ; + lv2:designation <http://lv2plug.in/ns/ext/port-groups#center> ; + lv2:index 2 ; + lv2:name "Audio In" ; + lv2:symbol "audio_in" ; + a lv2:AudioPort , + lv2:InputPort . + +<audio_out> + ingen:canvasX 186.0 ; + ingen:canvasY 60.0 ; + ingen:polyphonic false ; + lv2:designation <http://lv2plug.in/ns/ext/port-groups#center> ; + lv2:index 3 ; + lv2:name "Audio Out" ; + lv2:symbol "audio_out" ; + a lv2:AudioPort , + lv2:OutputPort . diff --git a/bundles/MonoEffect.ingen/manifest.ttl b/bundles/MonoEffect.ingen/manifest.ttl new file mode 100644 index 00000000..4484811a --- /dev/null +++ b/bundles/MonoEffect.ingen/manifest.ttl @@ -0,0 +1,16 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<MonoEffect.ttl> + lv2:prototype ingen:GraphPrototype ; + a ingen:Graph , + lv2:Plugin ; + rdfs:seeAlso <MonoEffect.ttl> . diff --git a/bundles/MonoInstrument.ingen/MonoInstrument.ttl b/bundles/MonoInstrument.ingen/MonoInstrument.ttl new file mode 100644 index 00000000..f8a8595d --- /dev/null +++ b/bundles/MonoInstrument.ingen/MonoInstrument.ttl @@ -0,0 +1,153 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<> + ingen:arc [ + ingen:head <notify> ; + ingen:tail <control> + ] , [ + ingen:head <Note/input> ; + ingen:tail <control> + ] ; + ingen:block <Note> ; + ingen:polyphony 1 ; + <http://lv2plug.in/ns/extensions/ui#ui> ingen:GraphUIGtk2 ; + lv2:extensionData <http://lv2plug.in/ns/ext/state#interface> ; + lv2:port <audio_out> , + <control> , + <notify> ; + doap:name "Ingen Mono Instrument Template" ; + a ingen:Graph , + lv2:InstrumentPlugin , + lv2:Plugin . + +<Note> + ingen:canvasX 206.5 ; + ingen:canvasY 8.0 ; + ingen:polyphonic true ; + lv2:port <Note/bend> , + <Note/frequency> , + <Note/gate> , + <Note/input> , + <Note/number> , + <Note/pressure> , + <Note/trigger> , + <Note/velocity> ; + lv2:prototype <http://drobilla.net/ns/ingen-internals#Note> ; + a ingen:Block . + +<Note/bend> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:default 0.0 ; + lv2:maximum 1.0 ; + lv2:minimum -1.0 ; + lv2:name "Bender" ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/frequency> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:maximum 25088.0 ; + lv2:minimum 16.0 ; + lv2:name "Frequency" ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/gate> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:name "Gate" ; + lv2:portProperty lv2:toggled ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/input> + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent ; + lv2:name "Input" ; + a atom:AtomPort , + lv2:InputPort . + +<Note/number> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:maximum 127.0 ; + lv2:minimum 0.0 ; + lv2:name "Number" ; + lv2:portProperty lv2:integer ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/pressure> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:default 0.0 ; + lv2:maximum 1.0 ; + lv2:minimum 0.0 ; + lv2:name "Pressure" ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/trigger> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:name "Trigger" ; + lv2:portProperty lv2:toggled ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/velocity> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:maximum 1.0 ; + lv2:minimum 0.0 ; + lv2:name "Velocity" ; + a atom:AtomPort , + lv2:OutputPort . + +<audio_out> + ingen:canvasX 506.0 ; + ingen:canvasY 118.5 ; + lv2:designation <http://lv2plug.in/ns/ext/port-groups#center> ; + lv2:index 2 ; + lv2:name "Audio Out" ; + lv2:symbol "audio_out" ; + a lv2:AudioPort , + lv2:OutputPort . + +<control> + ingen:canvasX 14.5 ; + ingen:canvasY 136.5 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:index 0 ; + lv2:name "Control" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "control" ; + a atom:AtomPort , + lv2:InputPort . + +<notify> + ingen:canvasX 214.5 ; + ingen:canvasY 260.5 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:index 1 ; + lv2:name "Control" ; + lv2:symbol "notify" ; + a atom:AtomPort , + lv2:OutputPort . diff --git a/bundles/MonoInstrument.ingen/manifest.ttl b/bundles/MonoInstrument.ingen/manifest.ttl new file mode 100644 index 00000000..a65a5341 --- /dev/null +++ b/bundles/MonoInstrument.ingen/manifest.ttl @@ -0,0 +1,16 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<MonoInstrument.ttl> + lv2:prototype ingen:GraphPrototype ; + a ingen:Graph , + lv2:Plugin ; + rdfs:seeAlso <MonoInstrument.ttl> . diff --git a/bundles/StereoEffect.ingen/StereoEffect.ttl b/bundles/StereoEffect.ingen/StereoEffect.ttl new file mode 100644 index 00000000..fff6ffce --- /dev/null +++ b/bundles/StereoEffect.ingen/StereoEffect.ttl @@ -0,0 +1,103 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<> + ingen:arc [ + ingen:head <left_out> ; + ingen:tail <left_in> + ] , [ + ingen:head <notify> ; + ingen:tail <control> + ] , [ + ingen:head <right_out> ; + ingen:tail <right_in> + ] ; + ingen:polyphony 1 ; + <http://lv2plug.in/ns/extensions/ui#ui> ingen:GraphUIGtk2 ; + lv2:extensionData <http://lv2plug.in/ns/ext/state#interface> ; + lv2:port <control> , + <notify> , + <left_in> , + <left_out> , + <right_in> , + <right_out> ; + doap:name "Ingen Stereo Effect Template" ; + a ingen:Graph , + lv2:Plugin . + +<control> + ingen:canvasX 9.5 ; + ingen:canvasY 112.0 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:index 0 ; + lv2:name "Control" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "control" ; + a atom:AtomPort , + lv2:InputPort . + +<notify> + ingen:canvasX 187.5 ; + ingen:canvasY 112.0 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:index 1 ; + lv2:name "Control" ; + lv2:symbol "notify" ; + a atom:AtomPort , + lv2:OutputPort . + +<left_in> + ingen:canvasX 13.0 ; + ingen:canvasY 60.0 ; + ingen:polyphonic false ; + lv2:designation <http://lv2plug.in/ns/ext/port-groups#left> ; + lv2:index 2 ; + lv2:name "Left In" ; + lv2:symbol "left_in" ; + a lv2:AudioPort , + lv2:InputPort . + +<left_out> + ingen:canvasX 186.0 ; + ingen:canvasY 60.0 ; + ingen:polyphonic false ; + lv2:designation <http://lv2plug.in/ns/ext/port-groups#left> ; + lv2:index 3 ; + lv2:name "Left Out" ; + lv2:symbol "left_out" ; + a lv2:AudioPort , + lv2:OutputPort . + +<right_in> + ingen:canvasX 8.0 ; + ingen:canvasY 8.0 ; + ingen:polyphonic false ; + lv2:designation <http://lv2plug.in/ns/ext/port-groups#right> ; + lv2:index 4 ; + lv2:name "Right In" ; + lv2:symbol "right_in" ; + a lv2:AudioPort , + lv2:InputPort . + +<right_out> + ingen:canvasX 181.0 ; + ingen:canvasY 8.0 ; + ingen:polyphonic false ; + lv2:designation <http://lv2plug.in/ns/ext/port-groups#right> ; + lv2:index 5 ; + lv2:name "Right Out" ; + lv2:symbol "right_out" ; + a lv2:AudioPort , + lv2:OutputPort . diff --git a/bundles/StereoEffect.ingen/manifest.ttl b/bundles/StereoEffect.ingen/manifest.ttl new file mode 100644 index 00000000..5c55ef41 --- /dev/null +++ b/bundles/StereoEffect.ingen/manifest.ttl @@ -0,0 +1,16 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<StereoEffect.ttl> + lv2:prototype ingen:GraphPrototype ; + a ingen:Graph , + lv2:Plugin ; + rdfs:seeAlso <StereoEffect.ttl> . diff --git a/bundles/StereoInstrument.ingen/StereoInstrument.ttl b/bundles/StereoInstrument.ingen/StereoInstrument.ttl new file mode 100644 index 00000000..84c756c1 --- /dev/null +++ b/bundles/StereoInstrument.ingen/StereoInstrument.ttl @@ -0,0 +1,165 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<Note> + ingen:canvasX 136.0 ; + ingen:canvasY 92.0 ; + ingen:polyphonic true ; + lv2:port <Note/bend> , + <Note/frequency> , + <Note/gate> , + <Note/input> , + <Note/number> , + <Note/pressure> , + <Note/trigger> , + <Note/velocity> ; + lv2:prototype <http://drobilla.net/ns/ingen-internals#Note> ; + a ingen:Block . + +<Note/bend> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:default 0.0 ; + lv2:maximum 1.0 ; + lv2:minimum -1.0 ; + lv2:name "Bender" ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/frequency> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:maximum 25088.0 ; + lv2:minimum 16.0 ; + lv2:name "Frequency" ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/gate> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:name "Gate" ; + lv2:portProperty lv2:toggled ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/input> + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent ; + lv2:name "Input" ; + a atom:AtomPort , + lv2:InputPort . + +<Note/number> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:maximum 127.0 ; + lv2:minimum 0.0 ; + lv2:name "Number" ; + lv2:portProperty lv2:integer ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/pressure> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:default 0.0 ; + lv2:maximum 1.0 ; + lv2:minimum 0.0 ; + lv2:name "Pressure" ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/trigger> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:name "Trigger" ; + lv2:portProperty lv2:toggled ; + a atom:AtomPort , + lv2:OutputPort . + +<Note/velocity> + atom:bufferType atom:Sequence ; + atom:supports atom:Float ; + lv2:maximum 1.0 ; + lv2:minimum 0.0 ; + lv2:name "Velocity" ; + a atom:AtomPort , + lv2:OutputPort . + +<> + ingen:arc [ + ingen:head <notify> ; + ingen:tail <control> + ] , [ + ingen:head <Note/input> ; + ingen:tail <control> + ] ; + ingen:block <Note> ; + ingen:polyphony 1 ; + <http://lv2plug.in/ns/extensions/ui#ui> ingen:GraphUIGtk2 ; + lv2:extensionData <http://lv2plug.in/ns/ext/state#interface> ; + lv2:port <control> , + <notify> , + <left_out> , + <right_out> ; + doap:name "Ingen Stereo Instrument Template" ; + a ingen:Graph , + lv2:InstrumentPlugin , + lv2:Plugin . + +<control> + ingen:canvasX 9.5 ; + ingen:canvasY 14.0 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + atom:supports midi:MidiEvent ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:index 0 ; + lv2:name "Control" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "control" ; + a atom:AtomPort , + lv2:InputPort . + +<notify> + ingen:canvasX 187.5 ; + ingen:canvasY 14.0 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:index 1 ; + lv2:name "Control" ; + lv2:symbol "notify" ; + a atom:AtomPort , + lv2:OutputPort . + +<left_out> + ingen:canvasX 455.0 ; + ingen:canvasY 65.0 ; + ingen:polyphonic false ; + lv2:designation <http://lv2plug.in/ns/ext/port-groups#left> ; + lv2:index 2 ; + lv2:name "Left Out" ; + lv2:symbol "left_out" ; + a lv2:AudioPort , + lv2:OutputPort . + +<right_out> + ingen:canvasX 454.0 ; + ingen:canvasY 111.0 ; + ingen:polyphonic false ; + lv2:designation <http://lv2plug.in/ns/ext/port-groups#right> ; + lv2:index 3 ; + lv2:name "Right Out" ; + lv2:symbol "right_out" ; + a lv2:AudioPort , + lv2:OutputPort . diff --git a/bundles/StereoInstrument.ingen/manifest.ttl b/bundles/StereoInstrument.ingen/manifest.ttl new file mode 100644 index 00000000..d4cc271d --- /dev/null +++ b/bundles/StereoInstrument.ingen/manifest.ttl @@ -0,0 +1,16 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<StereoInstrument.ttl> + lv2:prototype ingen:GraphPrototype ; + a ingen:Graph , + lv2:Plugin ; + rdfs:seeAlso <StereoInstrument.ttl> . diff --git a/bundles/ingen.lv2/errors.ttl b/bundles/ingen.lv2/errors.ttl new file mode 100644 index 00000000..694d3f6b --- /dev/null +++ b/bundles/ingen.lv2/errors.ttl @@ -0,0 +1,171 @@ +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix ingerr: <http://drobilla.net/ns/ingen/errors#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +ingerr: + a owl:Ontology ; + owl:imports <http://lv2plug.in/ns/lv2core> ; + rdfs:label "Ingen Error Ontology" ; + rdfs:comment "Errors that the Ingen server may return" . + +ingerr:Error + a owl:Class ; + rdfs:label "An error" . + +ingerr:errorCode + a owl:DatatypeProperty ; + rdfs:range xsd:integer ; + rdfs:label "error code" ; + rdfs:comment "The numeric code of an error." . + +ingerr:formatString + a owl:DatatypeProperty ; + rdfs:range xsd:string ; + rdfs:label "format string" ; + rdfs:comment "A C-style format string for producing a message." . + +ingerr:UnknownError + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 1 ; + ingerr:formatString "Unknown error" . + +ingerr:BadIndex + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 2 ; + ingerr:formatString "Invalid index" . + +ingerr:BadObjectType + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 3 ; + ingerr:formatString "Invalid object type" . + +ingerr:BadRequest + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 4 ; + ingerr:formatString "Invalid request" . + +ingerr:BadURI + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 5 ; + ingerr:formatString "Invalid URI" . + +ingerr:BadValueType + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 6 ; + ingerr:formatString "Invalid value type" . + +ingerr:ClientNotFound + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 7 ; + ingerr:formatString "Client not found" . + +ingerr:CreationFailed + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 8 ; + ingerr:formatString "Creation failed" . + +ingerr:DirectionMismatch + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 9 ; + ingerr:formatString "Direction mismatch" . + +ingerr:Exists + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 10 ; + ingerr:formatString "Object exists" . + +ingerr:InternalError + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 11 ; + ingerr:formatString "Internal error" . + +ingerr:InvalidParentPath + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 12 ; + ingerr:formatString "Invalid parent path" . + +ingerr:InvalidPoly + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 13 ; + ingerr:formatString "Invalid polyphony" . + +ingerr:NotDeletable + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 14 ; + ingerr:formatString "Object not deletable" . + +ingerr:NotFound + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 15 ; + ingerr:formatString "Object not found" . + +ingerr:NotMovable + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 16 ; + ingerr:formatString "Object not movable" . + +ingerr:NotPrepared + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 17 ; + ingerr:formatString "Not prepared" . + +ingerr:NoSpace + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 18 ; + ingerr:formatString "Insufficient space" . + +ingerr:ParentDiffers + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 19 ; + ingerr:formatString "Parent differs" . + +ingerr:ParentNotFound + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 20 ; + ingerr:formatString "Parent not found" . + +ingerr:PluginNotFound + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 21 ; + ingerr:formatString "Plugin not found" . + +ingerr:PortNotFound + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 22 ; + ingerr:formatString "Port not found" . + +ingerr:TypeMismatch + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 23 ; + ingerr:formatString "Type mismatch" . + +ingerr:UnknownType + a owl:Class ; + rdfs:subClassOf ingerr:Error ; + ingerr:errorCode 24 ; + ingerr:formatString "Unknown type" . diff --git a/bundles/ingen.lv2/ingen.ttl b/bundles/ingen.lv2/ingen.ttl new file mode 100644 index 00000000..9cf5e8c2 --- /dev/null +++ b/bundles/ingen.lv2/ingen.ttl @@ -0,0 +1,284 @@ +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix foaf: <http://xmlns.com/foaf/0.1/> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix ui: <http://lv2plug.in/ns/extensions/ui#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +ingen: + a owl:Ontology ; + owl:imports <http://lv2plug.in/ns/lv2core> ; + doap:name "Ingen" ; + rdfs:label "Ingen" ; + rdfs:comment "Ingen is a modular audio processing environment. This vocabulary describes an Ingen configuration, and is used in both the control protocol and saved files. 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." . + +ingen:Plugin + a rdfs:Class ; + rdfs:label "Plugin" ; + rdfs:comment """A class which can be instantiated into a ingen:Block. A plugin has a set of input and output "ports". In practice this class is semantically equivalent to lv2:Plugin, it only exists to allow the ingen ontology to be useful for "plugins" that aren't semantically LV2 plugins. See the LV2 specification for details about the required properties (rdf:type, doap:name, doap:license, and lv2:port). """ . + +ingen:Graph + a rdfs:Class ; + rdfs:subClassOf ingen:Plugin ; + rdfs:label "Graph" ; + rdfs:comment "A collection of Blocks connected together. A Graph can itself be a Block within a parent Graph, and so on." . + +ingen:file + a rdf:Property , + owl:DatatypeProperty ; + rdfs:domain ingen:Graph ; + rdfs:range xsd:anyURI ; + rdfs:label "file" ; + rdfs:comment "The file a Graph was loaded from." . + +ingen:canvasX + a rdf:Property , + owl:DatatypeProperty ; + rdfs:range xsd:decimal ; + rdfs:label "canvas X" ; + rdfs:comment "The X coordinate of an item on a canvas." . + +ingen:canvasY + a rdf:Property , + owl:DatatypeProperty ; + rdfs:range xsd:decimal ; + rdfs:label "canvas Y" ; + rdfs:comment "The Y coordinate of an item on a canvas." . + +ingen:minRunLoad + a rdf:Property , + owl:DatatypeProperty ; + rdfs:range xsd:decimal ; + rdfs:label "minimum run load" ; + rdfs:comment "The minimum fraction of a cycle spent running DSP." . + +ingen:maxRunLoad + a rdf:Property , + owl:DatatypeProperty ; + rdfs:range xsd:decimal ; + rdfs:label "maximum run load" ; + rdfs:comment "The maximum fraction of a cycle spent running DSP." . + +ingen:meanRunLoad + a rdf:Property , + owl:DatatypeProperty ; + rdfs:range xsd:decimal ; + rdfs:label "mean run load" ; + rdfs:comment "The average fraction of a cycle spent running DSP." . + +ingen:block + a rdf:Property , + owl:ObjectProperty ; + rdfs:domain ingen:Graph ; + rdfs:range ingen:Block ; + rdfs:label "block" ; + rdfs:comment "Signifies a graph contains some block." . + +ingen:polyphony + a rdf:Property , + owl:DatatypeProperty ; + rdfs:domain ingen:Graph ; + rdfs:range xsd:integer ; + rdfs:label "polyphony" ; + rdfs:comment """The amount of polyphony in a Graph. This defines the number of voices present on all :polyphonic children of this graph. Because a Graph is also a Block, a Graph may have both :polyphony and :polyphonic properties. These specify different things: :polyphony specifies the voice count of the Graph's children, and :polyphonic specifies whether the graph is seen as polyphonic to the Graph's parent.""" . + +ingen:sprungLayout + a rdf:Property , + owl:DatatypeProperty ; + rdfs:domain ingen:Graph ; + rdfs:range xsd:boolean ; + rdfs:label "sprung layout" ; + rdfs:comment """Whether or not the graph has a "sprung" force-directed layout.""" . + +ingen:loadedBundle + a rdf:Property , + owl:ObjectProperty ; + rdfs:label "loaded bundle" ; + rdfs:comment "Whether or not a bundle is loaded into Ingen." . + +ingen:value + a rdf:Property , + owl:DatatypeProperty ; + rdfs:domain lv2:Port ; + rdfs:label "value" ; + rdfs:comment "The current value of a port." . + +ingen:Internal + a rdfs:Class ; + rdfs:subClassOf ingen:Plugin ; + rdfs:label "Internal" ; + rdfs:comment """An internal 'plugin'""" . + +ingen:Node + a rdfs:Class ; + rdfs:label "Node" ; + rdfs:comment "An element of a Graph. A Node always has a valid path and symbol, with the possible exception of the root graph which may not have a symbol depending on context. Ingen uses restricted paths and/or URIs built from valid lv2:symbol components, so the symbol of a Node may be inferred from its URI if no explicit lv2:symbol property is given." . + +ingen:uiEmbedded + a rdf:Property , + owl:DatatypeProperty ; + rdfs:range xsd:boolean ; + rdfs:label "UI embedded" ; + rdfs:comment """Whether or not the block's GUI is embedded.""" . + +lv2:Port + a rdfs:Class ; + rdfs:subClassOf ingen:Node . + +ingen:activity + a rdf:Property , + owl:DatatypeProperty ; + rdfs:domain lv2:Port ; + rdfs:label "activity" ; + rdfs:comment "Transient activity. This property is used in the protocol to communicate activity at ports, such as MIDI events or audio peaks. It should never be stored in persistent data." . + +ingen:broadcast + a rdf:Property , + owl:DatatypeProperty ; + rdfs:domain lv2:Port ; + rdfs:range xsd:boolean ; + rdfs:label "broadcast" ; + rdfs:comment """Whether or not the port's value or activity should be broadcast to clients.""" . + +ingen:polyphonic + a rdf:Property , + owl:DatatypeProperty ; + rdfs:range xsd:boolean ; + rdfs:label "polyphonic" ; + rdfs:comment """Signifies this node should be replicated when it is part of a polyphonic graph. The amount of polyphony (i.e. the number of voices) is determined by the ingen:polyphony property of the containing graph. This is a boolean property which defines whether the parent can access each voice individually: All nodes within a graph are either polyphonic or not from their parent's perspective. An Node may itself have "internal" polyphony but not be polyphonic according to this property, if those voices are mixed down.""" . + +ingen:Block + a rdfs:Class ; + rdfs:subClassOf ingen:Node , + lv2:PluginBase ; + rdfs:label "Block" ; + rdfs:comment """A signal processing block, which is typically either a plugin instance, or a graph. + +A block MUST have at least one lv2:prototype property which is a subclass of lv2:Plugin. When there are many such properties, an applications SHOULD use the most specific class it understands.""" . + +ingen:enabled + a rdf:Property , + owl:DatatypeProperty ; + rdfs:domain ingen:Block ; + rdfs:range xsd:boolean ; + rdfs:label "enabled" ; + rdfs:comment "Signifies the block is or should be running." . + +ingen:prototype + a rdf:Property , + owl:ObjectProperty ; + owl:deprecated true ; + rdfs:domain ingen:Block ; + rdfs:label "prototype" ; + rdfs:comment "The object which this block is an instance of, or derived from." . + +ingen:Arc + a rdfs:Class ; + rdfs:label "Arc" ; + rdfs:subClassOf [ + a owl:Restriction ; + owl:onProperty ingen:tail ; + owl:allValuesFrom lv2:Port ; + owl:cardinality 1 ; + rdfs:comment "MUST have exactly one tail which is a lv2:Port." + ] , [ + a owl:Restriction ; + owl:onProperty ingen:head ; + owl:allValuesFrom lv2:Port ; + owl:cardinality 1 ; + rdfs:comment "MUST have exactly one head which is a lv2:Port." + ] ; + rdfs:comment "A connection between two ports. Graphs have a set of arcs which define how its component blocks and ports are connected." . + +ingen:arc + a rdf:Property , + owl:ObjectProperty ; + rdfs:domain ingen:Graph ; + rdfs:range ingen:Arc ; + rdfs:label "arc" ; + rdfs:comment "An arc contained in this graph." . + +ingen:tail + a rdf:Property , + owl:ObjectProperty , + owl:FunctionalProperty ; + rdfs:domain ingen:Arc ; + rdfs:range lv2:Port ; + rdfs:label "tail" ; + rdfs:comment "The source/sending port of this arc" . + +ingen:head + a rdf:Property , + owl:ObjectProperty , + owl:FunctionalProperty ; + rdfs:domain ingen:Arc ; + rdfs:range lv2:Port ; + rdfs:label "head" ; + rdfs:comment "The destination/receiving/sink port of this arc" . + +ingen:incidentTo + a rdf:Property , + owl:ObjectProperty , + owl:FunctionalProperty ; + rdfs:domain ingen:Arc ; + rdfs:label "incident to" ; + rdfs:comment "A special property used to describe any arc incident to a port or block. This is never saved in graph files, but is used in the control protocol to completely disconnect a Block or Port." . + +ingen:Undo + a rdfs:Class ; + rdfs:label "Undo" ; + rdfs:comment "A request to undo the previous change." . + +ingen:BundleStart + a rdfs:Class ; + rdfs:label "Bundle Start" ; + rdfs:comment "The start of an undo transaction." . + +ingen:BundleEnd + a rdfs:Class ; + rdfs:label "Bundle End" ; + rdfs:comment "The end of an undo transaction." . + +ingen:Option + a rdfs:Class ; + rdfs:subClassOf rdf:Property ; + rdfs:label "Ingen Option" . + +ingen:shortSwitch + a rdf:Property , + owl:DatatypeProperty , + owl:FunctionalProperty ; + rdfs:domain ingen:Option ; + rdfs:range xsd:string ; + rdfs:label "short switch" ; + rdfs:comment "Single character switch for short command line argument." . + +ingen:longSwitch + a rdf:Property , + owl:DatatypeProperty , + owl:FunctionalProperty ; + rdfs:domain ingen:Option ; + rdfs:range xsd:string ; + rdfs:label "long switch" ; + rdfs:comment "Lowercase, hyphenated switch for long command line argument." . + +ingen:numThreads + a rdf:Property , + owl:ObjectProperty , + ingen:Option ; + rdfs:label "number of threads" ; + ingen:shortSwitch "p" ; + ingen:longSwitch "threads" . + +ingen:externalContext + a rdfs:Resource ; + rdfs:label "external context" ; + rdfs:comment """The context for externally visible Graph properties, that is, properties which apply to the Graph when viewed as a Block within its parent Graph and should be saved in the parent's description.""" . + +ingen:internalContext + a rdfs:Resource ; + rdfs:label "internal context" ; + rdfs:comment """The context for internally visible Graph properties, that is, properties which are only relevant inside the graph and should be saved in the Graph's description.""" . diff --git a/bundles/ingen.lv2/internals.ttl b/bundles/ingen.lv2/internals.ttl new file mode 100644 index 00000000..53e828f7 --- /dev/null +++ b/bundles/ingen.lv2/internals.ttl @@ -0,0 +1,33 @@ +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix foaf: <http://xmlns.com/foaf/0.1/> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix internals: <http://drobilla.net/ns/ingen-internals#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema> . + +internals:Controller + a ingen:Internal ; + rdfs:label "Controller" ; + rdfs:comment """Outputs the value of a specific MIDI control as a signal. The output value will be scaled to be between the range specified by the minimum and maximum controls.""" . + +internals:Trigger + a ingen:Internal ; + rdfs:label "Trigger" ; + rdfs:comment """Outputs a gate, trigger, and velocity signal whenever the specified note is received. This is particularly useful for building percussive instruments.""" . + +internals:Note + a ingen:Internal ; + rdfs:label "Note" ; + rdfs:comment """Outputs the attributes of a note as signals. Typically the frequency output controls an oscillator and the gate and trigger control an envelope. This plugin is special because it is internally aware of polyphony and controls voice allocation.""" . + +internals:Time + a ingen:Internal ; + rdfs:label "Time" ; + rdfs:comment """Emits time and transport information like tempo, time signature, and speed. The emitted events are in the standard LV2 format expected by transport-aware LV2 plugins.""" . + +internals:BlockDelay + a ingen:Internal ; + rdfs:label "Block Delay" ; + rdfs:comment """Special internal delay block. This delays its input one full process cycle (or 'block'). It is necessary to have at least one block delay in any cycle in the graph, i.e. any feedback loops must contain a block delay.""" .
\ No newline at end of file diff --git a/bundles/ingen.lv2/manifest.ttl b/bundles/ingen.lv2/manifest.ttl new file mode 100644 index 00000000..12d3621a --- /dev/null +++ b/bundles/ingen.lv2/manifest.ttl @@ -0,0 +1,43 @@ +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix internals: <http://drobilla.net/ns/ingen-internals#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix ui: <http://lv2plug.in/ns/extensions/ui#> . + +ingen: + a owl:Ontology ; + rdfs:seeAlso <ingen.ttl> . + +ingen:GraphPrototype + a lv2:PluginBase ; + lv2:binary <libingen_lv2.so> ; + rdfs:comment """Template for all Ingen graphs. + +Saved Ingen graphs always set this as their lv2:prototype. When Ingen is +installed, a bundle is installed which included the Ingen LV2 binary and a +description of ingen:GraphPrototype which links to it. This way, Ingen graphs +can be loaded by LV2 hosts without including binaries or symbolic links in +saved Ingen bundles. +""" . + +ingen:GraphUIGtk2 + a ui:GtkUI ; + ui:binary <libingen_gui_lv2.so> ; + rdfs:comment "The Ingen patcher interface." . + +internals:Controller + a ingen:Plugin ; + rdfs:seeAlso <internals.ttl> . + +internals:Trigger + a ingen:Plugin ; + rdfs:seeAlso <internals.ttl> . + +internals:Note + a ingen:Plugin ; + rdfs:seeAlso <internals.ttl> . + +internals:Transport + a ingen:Plugin ; + rdfs:seeAlso <internals.ttl> . diff --git a/doc/ingen.1 b/doc/ingen.1 new file mode 100644 index 00000000..097404b9 --- /dev/null +++ b/doc/ingen.1 @@ -0,0 +1,85 @@ +.TH INGEN 1 "04 Apr 2015" + +.SH NAME +.B ingen \- A realtime modular audio processor + +.SH SYNOPSIS +ingen [OPTION]... + +.SH OPTIONS + +.TP +\fB\-a, \-\-atomic\-bundles\fR +Execute bundles atomically +.TP +\fB\-C, \-\-client\-port\fR=\fIINT\fR +Client port +.TP +\fB\-c, \-\-connect\fR=\fISTRING\fR +Connect to engine URI +\fB\-d, \-\-dump\fR +Print debug output +.TP +\fB\-t, \-\-trace\fR +Show LV2 plugin trace messages +.TP +\fB\-e, \-\-engine\fR +Run (JACK) engine +.TP +\fB\-E, \-\-engine-port\fR=\fIINT\fR +Engine listen port +.TP +\fB\-\-graph\-directory\fR +Default directory for opening graphs +.TP +\fB\-g, \-\-gui\fR +Launch the GTK graphical interface +.TP +\fB\-h, \-\-help\fR +Print this help message +.TP +\fB\-\-human\-names\fR +Show human names in GUI +.TP +\fB\-n, \-\-jack\-name\fR=\fISTRING\fR +JACK name +.TP +\fB\-s, \-\-jack\-server\fR=\fISTRING\fR +JACK server name +.TP +\fB\-l, \-\-load\fR=\fISTRING\fR +Load graph +.TP +\fB\-L, \-\-path\fR=\fISTRING\fR +Target path for loaded graph +.TP +\fB\-\-port\-labels\fR +Show port labels in GUI +.TP +\fB\-q, \-\-queue-size\fR=\fIINT\fR +Event queue size +.TP +\fB\-r, \-\-run\fR +Run script +.TP +\fB\-S, \-\-socket\fR=\fISTRING\fR +Engine socket path +.TP +\fB\-u, \-\-uuid\fR=\fISTRING\fR +JACK session UUID +.TP +\fB\-V, \-\-version\fR +Print version information + +.SH AUTHOR +Ingen was written by David Robillard <d@drobilla.net> + +.SH COPYRIGHT +Copyright \(co 2005-2016 David Robillard. +License AGPLv3: GNU AGPL version 3 <https://www.gnu.org/licenses/agpl-3.0>. +.br +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law. + +.SH "SEE ALSO" +<http://drobilla.net/software/ingen> diff --git a/doc/reference.doxygen.in b/doc/reference.doxygen.in new file mode 100644 index 00000000..4673ef21 --- /dev/null +++ b/doc/reference.doxygen.in @@ -0,0 +1,2418 @@ +# Doxyfile 1.8.12 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all text +# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv +# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv +# for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = Ingen + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = @INGEN_VERSION@ + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = . + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = NO + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = YES + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name=value". For example adding "class=itcl::class" +# will allow you to use the command class in the itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, Javascript, +# C#, C, C++, D, PHP, Objective-C, Python, Fortran (fixed format Fortran: +# FortranFixed, free formatted Fortran: FortranFree, unknown formatted Fortran: +# Fortran. In the later case the parser tries to guess whether the code is fixed +# or free formatted code, this is the default for Fortran type files), VHDL. For +# instance to make doxygen treat .inc files as Fortran files (default is PHP), +# and .f files as C (default is Fortran), use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 0. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 0 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = YES + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = NO + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# (class|struct|union) declarations. If set to NO, these declarations will be +# included in the documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = NO + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = YES + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= NO + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if <section_label> ... \endif and \cond <section_label> +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = NO + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = YES + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = NO + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = NO + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = @INGEN_SRCDIR@/ingen \ + @INGEN_SRCDIR@/src \ + @INGEN_SRCDIR@/ingen/client \ + @INGEN_SRCDIR@/src/server/events/ + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf and *.qsf. + +FILE_PATTERNS = + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# <filter> <input-file> +# +# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# function all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = YES + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = YES + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see http://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = NO + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 1 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = @INGEN_SRCDIR@/doc/style.css + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 160 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 40 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = YES + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: http://developer.apple.com/tools/xcode/), introduced with +# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = YES + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 1 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# http://www.mathjax.org) which uses client side Javascript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from http://www.mathjax.org before deployment. +# The default value is: http://cdn.mathjax.org/mathjax/latest. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use <access key> + S +# (what the <access key> is depends on the OS and browser, but it is typically +# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down +# key> to jump into the search results window, the results can be navigated +# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel +# the search. The filter options can be selected when the cursor is inside the +# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys> +# to select a filter and <Enter> or <escape> to activate or cancel the filter +# option. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +SEARCHENGINE = NO + +# When the SERVER_BASED_SEARCH tag is enabled the search engine will be +# implemented using a web server instead of a web client using Javascript. There +# are two flavors of web server based searching depending on the EXTERNAL_SEARCH +# setting. When disabled, doxygen will generate a PHP script for searching and +# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing +# and searching needs to be provided by external tools. See the section +# "External Indexing and Searching" for details. +# The default value is: NO. +# This tag requires that the tag SEARCHENGINE is set to YES. + +SERVER_BASED_SEARCH = NO + +# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP +# script for searching. Instead the search results are written to an XML file +# which needs to be processed by an external indexer. Doxygen will invoke an +# external search engine pointed to by the SEARCHENGINE_URL option to obtain the +# search results. +# +# Doxygen ships with an example indexer (doxyindexer) and search engine +# (doxysearch.cgi) which are based on the open source search engine library +# Xapian (see: http://xapian.org/). +# +# See the section "External Indexing and Searching" for details. +# The default value is: NO. +# This tag requires that the tag SEARCHENGINE is set to YES. + +EXTERNAL_SEARCH = NO + +# The SEARCHENGINE_URL should point to a search engine hosted by a web server +# which will return the search results when EXTERNAL_SEARCH is enabled. +# +# Doxygen ships with an example indexer (doxyindexer) and search engine +# (doxysearch.cgi) which are based on the open source search engine library +# Xapian (see: http://xapian.org/). See the section "External Indexing and +# Searching" for details. +# This tag requires that the tag SEARCHENGINE is set to YES. + +SEARCHENGINE_URL = + +# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed +# search data is written to a file for indexing by an external tool. With the +# SEARCHDATA_FILE tag the name of this file can be specified. +# The default file is: searchdata.xml. +# This tag requires that the tag SEARCHENGINE is set to YES. + +SEARCHDATA_FILE = searchdata.xml + +# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the +# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is +# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple +# projects and redirect the results back to the right project. +# This tag requires that the tag SEARCHENGINE is set to YES. + +EXTERNAL_SEARCH_ID = + +# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen +# projects other than the one defined by this configuration file, but that are +# all added to the same external search index. Each project needs to have a +# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of +# to a relative location where the documentation can be found. The format is: +# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ... +# This tag requires that the tag SEARCHENGINE is set to YES. + +EXTRA_SEARCH_MAPPINGS = + +#--------------------------------------------------------------------------- +# Configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. +# The default value is: YES. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: latex. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. +# +# Note that when enabling USE_PDFLATEX this option is only used for generating +# bitmaps for formulas in the HTML output, but not in the Makefile that is +# written to the output directory. +# The default file is: latex. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate +# index for LaTeX. +# The default file is: makeindex. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX +# documents. This may be useful for small projects and may help to save some +# trees in general. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +COMPACT_LATEX = YES + +# The PAPER_TYPE tag can be used to set the paper type that is used by the +# printer. +# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x +# 14 inches) and executive (7.25 x 10.5 inches). +# The default value is: a4. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +PAPER_TYPE = a4wide + +# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names +# that should be included in the LaTeX output. The package can be specified just +# by its name or with the correct syntax as to be used with the LaTeX +# \usepackage command. To get the times font for instance you can specify : +# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times} +# To use the option intlimits with the amsmath package you can specify: +# EXTRA_PACKAGES=[intlimits]{amsmath} +# If left blank no extra packages will be included. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the +# generated LaTeX document. The header should contain everything until the first +# chapter. If it is left blank doxygen will generate a standard header. See +# section "Doxygen usage" for information on how to let doxygen write the +# default header to a separate file. +# +# Note: Only use a user-defined header if you know what you are doing! The +# following commands have a special meaning inside the header: $title, +# $datetime, $date, $doxygenversion, $projectname, $projectnumber, +# $projectbrief, $projectlogo. Doxygen will replace $title with the empty +# string, for the replacement values of the other commands the user is referred +# to HTML_HEADER. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_HEADER = + +# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the +# generated LaTeX document. The footer should contain everything after the last +# chapter. If it is left blank doxygen will generate a standard footer. See +# LATEX_HEADER for more information on how to generate a default footer and what +# special commands can be used inside the footer. +# +# Note: Only use a user-defined footer if you know what you are doing! +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_FOOTER = + +# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# LaTeX style sheets that are included after the standard style sheets created +# by doxygen. Using this option one can overrule certain style aspects. Doxygen +# will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_EXTRA_STYLESHEET = + +# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the LATEX_OUTPUT output +# directory. Note that the files will be copied as-is; there are no commands or +# markers available. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_EXTRA_FILES = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is +# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will +# contain links (just like the HTML output) instead of page references. This +# makes the output suitable for online browsing using a PDF viewer. +# The default value is: YES. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +PDF_HYPERLINKS = YES + +# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate +# the PDF file directly from the LaTeX files. Set this option to YES, to get a +# higher quality PDF documentation. +# The default value is: YES. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +USE_PDFLATEX = YES + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode +# command to the generated LaTeX files. This will instruct LaTeX to keep running +# if errors occur, instead of asking the user for help. This option is also used +# when generating formulas in HTML. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_BATCHMODE = NO + +# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the +# index chapters (such as File Index, Compound Index, etc.) in the output. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_HIDE_INDICES = YES + +# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source +# code with syntax highlighting in the LaTeX output. +# +# Note that which sources are shown also depends on other settings such as +# SOURCE_BROWSER. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_SOURCE_CODE = NO + +# The LATEX_BIB_STYLE tag can be used to specify the style to use for the +# bibliography, e.g. plainnat, or ieeetr. See +# http://en.wikipedia.org/wiki/BibTeX and \cite for more info. +# The default value is: plain. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_BIB_STYLE = plain + +# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated +# page will contain the date and time when the page was generated. Setting this +# to NO can help when comparing the output of multiple runs. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_TIMESTAMP = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The +# RTF output is optimized for Word 97 and may not look too pretty with other RTF +# readers/editors. +# The default value is: NO. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: rtf. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF +# documents. This may be useful for small projects and may help to save some +# trees in general. +# The default value is: NO. +# This tag requires that the tag GENERATE_RTF is set to YES. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will +# contain hyperlink fields. The RTF file will contain links (just like the HTML +# output) instead of page references. This makes the output suitable for online +# browsing using Word or some other Word compatible readers that support those +# fields. +# +# Note: WordPad (write) and others do not support links. +# The default value is: NO. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_HYPERLINKS = NO + +# Load stylesheet definitions from file. Syntax is similar to doxygen's config +# file, i.e. a series of assignments. You only have to provide replacements, +# missing definitions are set to their default value. +# +# See also section "Doxygen usage" for information on how to generate the +# default style sheet that doxygen normally uses. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an RTF document. Syntax is +# similar to doxygen's config file. A template extensions file can be generated +# using doxygen -e rtf extensionFile. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_EXTENSIONS_FILE = + +# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code +# with syntax highlighting in the RTF output. +# +# Note that which sources are shown also depends on other settings such as +# SOURCE_BROWSER. +# The default value is: NO. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_SOURCE_CODE = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for +# classes and files. +# The default value is: NO. + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. A directory man3 will be created inside the directory specified by +# MAN_OUTPUT. +# The default directory is: man. +# This tag requires that the tag GENERATE_MAN is set to YES. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to the generated +# man pages. In case the manual section does not start with a number, the number +# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is +# optional. +# The default value is: .3. +# This tag requires that the tag GENERATE_MAN is set to YES. + +MAN_EXTENSION = .3 + +# The MAN_SUBDIR tag determines the name of the directory created within +# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by +# MAN_EXTENSION with the initial . removed. +# This tag requires that the tag GENERATE_MAN is set to YES. + +MAN_SUBDIR = + +# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it +# will generate one additional man file for each entity documented in the real +# man page(s). These additional files only source the real man page, but without +# them the man command would be unable to find the correct page. +# The default value is: NO. +# This tag requires that the tag GENERATE_MAN is set to YES. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that +# captures the structure of the code including all documentation. +# The default value is: NO. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: xml. +# This tag requires that the tag GENERATE_XML is set to YES. + +XML_OUTPUT = xml + +# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program +# listings (including syntax highlighting and cross-referencing information) to +# the XML output. Note that enabling this will significantly increase the size +# of the XML output. +# The default value is: YES. +# This tag requires that the tag GENERATE_XML is set to YES. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the DOCBOOK output +#--------------------------------------------------------------------------- + +# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files +# that can be used to generate PDF. +# The default value is: NO. + +GENERATE_DOCBOOK = NO + +# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in +# front of it. +# The default directory is: docbook. +# This tag requires that the tag GENERATE_DOCBOOK is set to YES. + +DOCBOOK_OUTPUT = docbook + +# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the +# program listings (including syntax highlighting and cross-referencing +# information) to the DOCBOOK output. Note that enabling this will significantly +# increase the size of the DOCBOOK output. +# The default value is: NO. +# This tag requires that the tag GENERATE_DOCBOOK is set to YES. + +DOCBOOK_PROGRAMLISTING = NO + +#--------------------------------------------------------------------------- +# Configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an +# AutoGen Definitions (see http://autogen.sf.net) file that captures the +# structure of the code including all documentation. Note that this feature is +# still experimental and incomplete at the moment. +# The default value is: NO. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module +# file that captures the structure of the code including all documentation. +# +# Note that this feature is still experimental and incomplete at the moment. +# The default value is: NO. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary +# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI +# output from the Perl module output. +# The default value is: NO. +# This tag requires that the tag GENERATE_PERLMOD is set to YES. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely +# formatted so it can be parsed by a human reader. This is useful if you want to +# understand what is going on. On the other hand, if this tag is set to NO, the +# size of the Perl module output will be much smaller and Perl will parse it +# just the same. +# The default value is: YES. +# This tag requires that the tag GENERATE_PERLMOD is set to YES. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file are +# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful +# so different doxyrules.make files included by the same Makefile don't +# overwrite each other's variables. +# This tag requires that the tag GENERATE_PERLMOD is set to YES. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all +# C-preprocessor directives found in the sources and include files. +# The default value is: YES. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names +# in the source code. If set to NO, only conditional compilation will be +# performed. Macro expansion can be done in a controlled way by setting +# EXPAND_ONLY_PREDEF to YES. +# The default value is: NO. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +MACRO_EXPANSION = NO + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then +# the macro expansion is limited to the macros specified with the PREDEFINED and +# EXPAND_AS_DEFINED tags. +# The default value is: NO. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES, the include files in the +# INCLUDE_PATH will be searched if a #include is found. +# The default value is: YES. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by the +# preprocessor. +# This tag requires that the tag SEARCH_INCLUDES is set to YES. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will be +# used. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that are +# defined before the preprocessor is started (similar to the -D option of e.g. +# gcc). The argument of the tag is a list of macros of the form: name or +# name=definition (no spaces). If the definition and the "=" are omitted, "=1" +# is assumed. To prevent a macro definition from being undefined via #undef or +# recursively expanded use the := operator instead of the = operator. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +PREDEFINED = + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this +# tag can be used to specify a list of macro names that should be expanded. The +# macro definition that is found in the sources will be used. Use the PREDEFINED +# tag if you want to use a different macro definition that overrules the +# definition found in the source code. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will +# remove all references to function-like macros that are alone on a line, have +# an all uppercase name, and do not end with a semicolon. Such function macros +# are typically used for boiler-plate code, and will confuse the parser if not +# removed. +# The default value is: YES. +# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration options related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES tag can be used to specify one or more tag files. For each tag +# file the location of the external documentation should be added. The format of +# a tag file without this location is as follows: +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where loc1 and loc2 can be relative or absolute paths or URLs. See the +# section "Linking to external documentation" for more information about the use +# of tag files. +# Note: Each tag file must have a unique name (where the name does NOT include +# the path). If a tag file is not located in the directory in which doxygen is +# run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create a +# tag file that is based on the input files it reads. See section "Linking to +# external documentation" for more information about the usage of tag files. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES, all external class will be listed in +# the class index. If set to NO, only the inherited external classes will be +# listed. +# The default value is: NO. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will be +# listed. +# The default value is: YES. + +EXTERNAL_GROUPS = YES + +# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in +# the related pages index. If set to NO, only the current project's pages will +# be listed. +# The default value is: YES. + +EXTERNAL_PAGES = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of 'which perl'). +# The default file (with absolute path) is: /usr/bin/perl. + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram +# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to +# NO turns the diagrams off. Note that this option also works with HAVE_DOT +# disabled, but it is recommended to install and use dot, since it yields more +# powerful graphs. +# The default value is: YES. + +CLASS_DIAGRAMS = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. Doxygen will then run the mscgen tool (see: +# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the +# documentation. The MSCGEN_PATH tag allows you to specify the directory where +# the mscgen tool resides. If left empty the tool is assumed to be found in the +# default search path. + +MSCGEN_PATH = + +# You can include diagrams made with dia in doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. + +DIA_PATH = + +# If set to YES the inheritance and collaboration graphs will hide inheritance +# and usage relations if the target is undocumented or is not a class. +# The default value is: YES. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz (see: +# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# Bell Labs. The other options in this section have no effect if this option is +# set to NO +# The default value is: NO. + +HAVE_DOT = YES + +# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed +# to run in parallel. When set to 0 doxygen will base this on the number of +# processors available in the system. You can set it explicitly to a value +# larger than 0 to get control over the balance between CPU load and processing +# speed. +# Minimum value: 0, maximum value: 32, default value: 0. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_NUM_THREADS = 0 + +# When you want a differently looking font in the dot files that doxygen +# generates you can specify the font name using DOT_FONTNAME. You need to make +# sure dot is able to find the font, which can be done by putting it in a +# standard location or by setting the DOTFONTPATH environment variable or by +# setting DOT_FONTPATH to the directory containing the font. +# The default value is: Helvetica. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_FONTNAME = + +# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of +# dot graphs. +# Minimum value: 4, maximum value: 24, default value: 10. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_FONTSIZE = 10 + +# By default doxygen will tell dot to use the default font as specified with +# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set +# the path where dot can find it using this tag. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_FONTPATH = + +# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for +# each documented class showing the direct and indirect inheritance relations. +# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a +# graph for each documented class showing the direct and indirect implementation +# dependencies (inheritance, containment, and class references variables) of the +# class with other documented classes. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for +# groups, showing the direct groups dependencies. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +UML_LOOK = NO + +# If the UML_LOOK tag is enabled, the fields and methods are shown inside the +# class node. If there are many fields or methods and many nodes the graph may +# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the +# number of items for each type to make the size more manageable. Set this to 0 +# for no limit. Note that the threshold may be exceeded by 50% before the limit +# is enforced. So when you set the threshold to 10, up to 15 fields may appear, +# but if the number exceeds 15, the total amount of fields shown is limited to +# 10. +# Minimum value: 0, maximum value: 100, default value: 10. +# This tag requires that the tag HAVE_DOT is set to YES. + +UML_LIMIT_NUM_FIELDS = 10 + +# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and +# collaboration graphs will show the relations between templates and their +# instances. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +TEMPLATE_RELATIONS = NO + +# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to +# YES then doxygen will generate a graph for each documented file showing the +# direct and indirect include dependencies of the file with other documented +# files. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +INCLUDE_GRAPH = YES + +# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are +# set to YES then doxygen will generate a graph for each documented file showing +# the direct and indirect include dependencies of the file with other documented +# files. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH tag is set to YES then doxygen will generate a call +# dependency graph for every global function or class method. +# +# Note that enabling this option will significantly increase the time of a run. +# So in most cases it will be better to enable call graphs for selected +# functions only using the \callgraph command. Disabling a call graph can be +# accomplished by means of the command \hidecallgraph. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +CALL_GRAPH = NO + +# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller +# dependency graph for every global function or class method. +# +# Note that enabling this option will significantly increase the time of a run. +# So in most cases it will be better to enable caller graphs for selected +# functions only using the \callergraph command. Disabling a caller graph can be +# accomplished by means of the command \hidecallergraph. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical +# hierarchy of all classes instead of a textual one. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the +# dependencies a directory has on other directories in a graphical way. The +# dependency relations are determined by the #include relations between the +# files in the directories. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. For an explanation of the image formats see the section +# output formats in the documentation of the dot tool (Graphviz (see: +# http://www.graphviz.org/)). +# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order +# to make the SVG files visible in IE 9+ (other browsers do not have this +# requirement). +# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo, +# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and +# png:gdiplus:gdiplus. +# The default value is: png. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_IMAGE_FORMAT = png + +# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to +# enable generation of interactive SVG images that allow zooming and panning. +# +# Note that this requires a modern browser other than Internet Explorer. Tested +# and working are Firefox, Chrome, Safari, and Opera. +# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make +# the SVG files visible. Older versions of IE do not have SVG support. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +INTERACTIVE_SVG = NO + +# The DOT_PATH tag can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the \dotfile +# command). +# This tag requires that the tag HAVE_DOT is set to YES. + +DOTFILE_DIRS = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = + +# The DIAFILE_DIRS tag can be used to specify one or more directories that +# contain dia files that are included in the documentation (see the \diafile +# command). + +DIAFILE_DIRS = + +# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the +# path where java can find the plantuml.jar file. If left blank, it is assumed +# PlantUML is not used or called during a preprocessing step. Doxygen will +# generate a warning when it encounters a \startuml command in this case and +# will not generate output for the diagram. + +PLANTUML_JAR_PATH = + +# When using plantuml, the specified paths are searched for files specified by +# the !include statement in a plantuml block. + +PLANTUML_INCLUDE_PATH = + +# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes +# that will be shown in the graph. If the number of nodes in a graph becomes +# larger than this value, doxygen will truncate the graph, which is visualized +# by representing a node as a red box. Note that doxygen if the number of direct +# children of the root node in a graph is already larger than +# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that +# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. +# Minimum value: 0, maximum value: 10000, default value: 50. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_GRAPH_MAX_NODES = 50 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs +# generated by dot. A depth value of 3 means that only nodes reachable from the +# root by following a path via at most 3 edges will be shown. Nodes that lay +# further from the root node will be omitted. Note that setting this option to 1 +# or 2 may greatly reduce the computation time needed for large code bases. Also +# note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. +# Minimum value: 0, maximum value: 1000, default value: 0. +# This tag requires that the tag HAVE_DOT is set to YES. + +MAX_DOT_GRAPH_DEPTH = 0 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, because dot on Windows does not seem +# to support this out of the box. +# +# Warning: Depending on the platform used, enabling this option may lead to +# badly anti-aliased labels on the edges of a graph (i.e. they become hard to +# read). +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_TRANSPARENT = YES + +# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) support +# this, this feature is disabled by default. +# The default value is: NO. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_MULTI_TARGETS = NO + +# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page +# explaining the meaning of the various boxes and arrows in the dot generated +# graphs. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot +# files that are used to generate the various graphs. +# The default value is: YES. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_CLEANUP = YES diff --git a/doc/style.css b/doc/style.css new file mode 100644 index 00000000..172be516 --- /dev/null +++ b/doc/style.css @@ -0,0 +1,1145 @@ +body { + max-width: 80em; + margin: 0; + margin-left: auto; + margin-right: auto; + background: #FFF; + color: #000; +} + +#titlearea { + display: none; +} + +h1 { + font-size: 180%; + font-weight: 900; +} + +h2 { + font-size: 140%; + font-weight: 700; +} + +h3 { + font-size: 120%; + font-weight: 700; +} + +h4 { + font-size: 110%; + font-weight: 700; +} + +h5 { + font-size: 100%; + font-weight: 700; +} + +h6 { + font-size: 100%; + font-weight: 600; +} + +p { + margin: 0 0 1em 0; +} + +dt { + font-weight: 700; +} + +p.startli,p.startdd,p.starttd { + margin-top: 2px; +} + +p.endli { + margin-bottom: 0; +} + +p.enddd { + margin-bottom: 4px; +} + +p.endtd { + margin-bottom: 2px; +} + +caption { + font-weight: 700; +} + +span.legend { + font-size: 70%; + text-align: center; +} + +h3.version { + font-size: 90%; + text-align: center; +} + +div.qindex,div.navtab { + background-color: #EBEFF6; + border: 1px solid #A3B4D7; + text-align: center; + margin: 2px; + padding: 2px; +} + +div.navtab { + margin-right: 15px; +} + +/* @group Link Styling */ +a { + color: #546E00; + text-decoration: none; +} + +.contents a:visited { + color: #344E00; +} + +a:hover { + text-decoration: underline; +} + +a.qindexHL { + background-color: #9CAFD4; + color: #FFF; + border: 1px double #869DCA; +} + +code { + color: #444; +} + +a.code { + color: #4665A2; +} + +a.codeRef { + color: #4665A2; +} + +/* @end */ +dl.el { + margin-left: -1cm; +} + +.fragment { + font-family: monospace, fixed; + font-size: 105%; + padding-bottom: 1em; +} + +pre.fragment { + border: 1px solid #C4C4C4; + background-color: #F9F9F9; + padding: 4px 6px; + margin: 4px 8px 4px 2px; + overflow: auto; + font-size: 9pt; + line-height: 125%; +} + +div.ah { + background-color: #000; + font-weight: 700; + color: #FFF; + margin-bottom: 3px; + margin-top: 3px; + padding: .2em; + border: thin solid #333; +} + +div.groupHeader { + margin-left: 16px; + margin-top: 12px; + margin-bottom: 6px; + font-weight: 700; +} + +a + h2.groupheader { + display: none; +} + +div.groupText { + margin-left: 16px; + font-style: italic; +} + +div.contents { + margin-top: 10px; + margin-left: 10px; + margin-right: 10px; +} + +td.indexkey { + background-color: #EBEFF6; + font-weight: 700; + border: 1px solid #C4CFE5; + margin: 2px 0; + padding: 2px 10px; +} + +td.indexvalue { + background-color: #EBEFF6; + border: 1px solid #C4CFE5; + padding: 2px 10px; + margin: 2px 0; +} + +tr.memlist { + background-color: #EEF1F7; +} + +p.formulaDsp { + text-align: center; +} + +img.formulaInl { + vertical-align: middle; +} + +div.center { + text-align: center; + margin-top: 0; + margin-bottom: 0; + padding: 0; +} + +div.center img { + border: 0; +} + +address.footer { + text-align: right; + padding-right: 12px; +} + +img.footer { + border: 0; + vertical-align: middle; +} + +/* @group Code Colorization */ +span.keyword { + color: green; +} + +span.keywordtype { + color: #3E873E; +} + +span.keywordflow { + color: #e08000; +} + +span.comment { + color: maroon; +} + +span.preprocessor { + color: #806020; +} + +span.stringliteral { + color: #002080; +} + +span.charliteral { + color: teal; +} + +span.vhdldigit { + color: #F0F; +} + +span.vhdlkeyword { + color: #700070; +} + +span.vhdllogic { + color: red; +} + +/* @end */ +td.tiny { + font-size: 75%; +} + +.dirtab { + padding: 4px; + border-collapse: collapse; + border: 1px solid #A3B4D7; +} + +th.dirtab { + background: #EBEFF6; + font-weight: 700; +} + +hr { + height: 0; + border: none; + border-top: 1px solid #DDD; + margin: 2em 0 1em; +} + +hr.footer { + height: 1px; +} + +/* @group Member Descriptions */ +table.memberdecls { + border-spacing: 0.125em; +} + +h2.groupheader { + margin: 1em 0 0.5em 0; +} + +.mdescLeft,.mdescRight,.memItemLeft,.memItemRight,.memTemplItemLeft,.memTemplItemRight,.memTemplParams { + margin: 0; + padding: 0; +} + +.mdescLeft,.mdescRight { + color: #555; +} + +.memItemLeft,.memItemRight,.memTemplParams { + border: 0; + font-family: monospace, fixed; + font-size: 90%; +} + +.memItemLeft,.memTemplItemLeft { + white-space: nowrap; + padding-left: 2em; + padding-right: 1em; +} + +.memItemLeft a.el { + font-weight: bold; +} + +.memTemplParams { + color: #464646; + white-space: nowrap; +} + +td.memSeparator { + display: none; +} + +td.mlabels-right { + vertical-align: top; + padding-top: 4px; + color: #AA6; +} + +.memtitle { + display: none; +} + +/* @end */ +/* @group Member Details */ +/* Styles for detailed member documentation */ +.memtemplate { + font-size: 80%; + color: #4665A2; + font-weight: bold; +} + +.memnav { + background-color: #EBEFF6; + border: 1px solid #A3B4D7; + text-align: center; + margin: 2px; + margin-right: 15px; + padding: 2px; +} + +.memitem { + padding: 0; + margin: 1em 0 1em 0; +} + +.memproto { + padding: 0; + font-size: 110%; + font-weight: bold; + color: #000; +} + +.memproto .paramname { + color: #444; + font-style: normal; +} + +.memdoc { + padding: 0 0 0.5em 2em; +} + +.paramkey { + text-align: right; +} + +.paramtype { + color: #3E873E; + white-space: nowrap; +} + +.paramname { + color: #444; + white-space: nowrap; + font-weight: bold; +} + +td.paramname { + vertical-align: top; +} + +.fieldname { + color: #000; +} + +td.fieldname { + padding-right: 1em; + vertical-align: top; +} + +td.fieldtype { + vertical-align: top; + color: #444; +} + +td.fielddoc p { + margin: 0; +} + +/* @end */ +/* @group Directory (tree) */ +/* for the tree view */ +.ftvtree { + font-family: sans-serif; + margin: 0; +} + +/* these are for tree view when used as main index */ +.directory { + font-size: small; + margin: 0.5em; +} + +.directory h3 { + margin: 0; + margin-top: 1em; + font-size: 11pt; +} + +.directory > h3 { + margin-top: 0; +} + +.directory p { + margin: 0; + white-space: nowrap; +} + +.directory div { + display: none; + margin: 0; +} + +.directory img { + vertical-align: -30%; +} + +/* these are for tree view when not used as main index */ +.directory-alt { + font-size: 100%; + font-weight: bold; +} + +.directory-alt h3 { + margin: 0; + margin-top: 1em; + font-size: 11pt; +} + +.directory-alt > h3 { + margin-top: 0; +} + +.directory-alt p { + margin: 0; + white-space: nowrap; +} + +.directory-alt div { + display: none; + margin: 0; +} + +.directory-alt img { + vertical-align: -30%; +} + +/* @end */ +div.dynheader { + margin-top: 8px; +} + +address { + font-style: normal; + color: #2A3D61; +} + +table.doxtable { + border-collapse: collapse; + margin: 0.5em; +} + +table.doxtable td,table.doxtable th { + border: 1px solid #DDD; + padding: 3px 7px 2px; +} + +table.doxtable th { + background-color: #F3F3F3; + color: #000; + padding-bottom: 4px; + padding-top: 5px; + text-align: left; + font-weight: bold; +} + +.tabsearch { + top: 0; + left: 10px; + height: 36px; + z-index: 101; + overflow: hidden; + font-size: 13px; +} + +div.navpath { + padding: 0.25em; +} + +.navpath ul { + font-size: x-small; + color: #8AA0CC; + overflow: hidden; + margin: 0; + padding: 0; +} + +.navpath li { + list-style-type: none; + float: left; + padding-left: 10px; + padding-right: 15px; + color: #364D7C; +} + +.navpath a { + display: block; + text-decoration: none; + outline: none; +} + +.navpath a:hover { + color: #6884BD; +} + +div.summary { + float: right; + font-size: x-small; + padding: 0.25em 0.5em 0 0; + width: 50%; + text-align: right; +} + +div.summary a { + white-space: nowrap; +} + +div.header { + background-color: #F3F3F3; + margin: 0; + border: 0; +} + +div.headertitle { + font-size: 180%; + font-weight: bold; + color: #FFF; + padding: 0.125em 0.25em 0.125em 0.25em; + background-color: #333; + background: linear-gradient(to bottom, #333 0%, #111 100%); + border: solid 1px #444; + border-top: 0; + border-radius: 0 0 6px 6px; +} + +div.line { + font-family: monospace, fixed; + font-size: 13px; + min-height: 13px; + line-height: 1.0; + text-wrap: avoid; + white-space: pre-wrap; + text-indent: -53px; + padding-left: 53px; + padding-bottom: 0; + margin: 0; +} + +.glow { + background-color: cyan; + box-shadow: 0 0 10px cyan; +} + +span.lineno { + padding-right: 4px; + text-align: right; + border-right: 2px solid #0F0; + background-color: #E8E8E8; + white-space: pre; +} +span.lineno a { + background-color: #D8D8D8; +} + +span.lineno a:hover { + background-color: #C8C8C8; +} + +.tabs, .tabs2, .navpath { + background-image: none; + background-color: #333; + background: linear-gradient(to bottom, #333 0%, #111 100%); + border: 0; + border-bottom: solid 2px #000; + padding: 0; + padding-top: 2px; + font-size: small; +} + +#navrow1 { + border: 0; +} + +th { + text-align: left; +} + +.mlabel { + padding: 0.125em; +} + +/* tabs*/ + +.tablist { + margin: 0; + padding: 0; + display: table; +} + +.tablist li { + display: table-cell; + line-height: 2em; + list-style: none; + background-color: #333; + background: linear-gradient(to bottom, #444 0%, #222 100%); + border: 1px solid #222; + border-bottom: 0; + border-radius: 6px 6px 0 0; + color: #DDD; +} + +.tablist a { + display: block; + padding: 0 20px; + font-weight: bold; + color: #859900; + text-decoration: none; + outline: none; +} + +.header a { + color: #859900; +} + +.tabs3 .tablist a { + padding: 0 10px; +} + +.tablist a:hover { + color: #fff; + text-decoration: none; +} + +.tablist li.current a { + color: #fff; +} + +span.icon { + display: none; +} + +/* nav bar */ + +.sm { + position: relative; + z-index: 9999; +} + +.sm,.sm ul,.sm li { + display: block; + list-style: none; + margin: 0; + padding: 0; + line-height: normal; + direction: ltr; + text-align: left; + -webkit-tap-highlight-color: rgba(0,0,0,0); +} + +.sm-rtl,.sm-rtl ul,.sm-rtl li { + direction: rtl; + text-align: right; +} + +.sm>li>h1,.sm>li>h2,.sm>li>h3,.sm>li>h4,.sm>li>h5,.sm>li>h6 { + margin: 0; + padding: 0; +} + +.sm ul { + display: none; +} + +.sm li,.sm a { + position: relative; +} + +.sm a { + display: block; +} + +.sm a.disabled { + cursor: not-allowed; +} + +.sm:after { + content: "\00a0"; + display: block; + height: 0; + font: 0/0 serif; + clear: both; + visibility: hidden; + overflow: hidden; +} + +.sm,.sm *,.sm :before,.sm :after { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +#doc-content { + overflow: auto; + display: block; + padding: 0; + margin: 0; + -webkit-overflow-scrolling: touch; +} + +.sm-dox { + background-image: none; + background-color: #333; + background: linear-gradient(to bottom, #333 0%, #111 100%); + color: #fff; +} + +.sm-dox a,.sm-dox a:focus,.sm-dox a:hover,.sm-dox a:active { + padding: 0 12px; + padding-right: 43px; + font-size: small; + font-weight: 600; + line-height: auto; + text-decoration: none; + text-shadow: none; + color: inherit; + outline: 0; +} + +.sm-dox a:hover { + background-image: none; + background-repeat: repeat-x; + color: inherit; + text-shadow: 0 1px 1px #000; +} + +.sm-dox a.current { + color: #d23600; +} + +.sm-dox a.disabled { + color: #bbb; +} + +.sm-dox a span.sub-arrow { + position: absolute; + top: 50%; + margin-top: -14px; + left: auto; + right: 3px; + width: 28px; + height: 28px; + overflow: hidden; + font: bold 12px/28px monospace !important; + text-align: center; + text-shadow: none; + background: rgba(255,255,255,0.5); + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +.sm-dox a.highlighted span.sub-arrow:before { + display: block; + content: '-'; +} + +.sm-dox>li:first-child>a,.sm-dox>li:first-child>:not(ul) a { + -moz-border-radius: 5px 5px 0 0; + -webkit-border-radius: 5px; + border-radius: 5px 5px 0 0; +} + +.sm-dox>li:last-child>a,.sm-dox>li:last-child>:not(ul) a,.sm-dox>li:last-child>ul,.sm-dox>li:last-child>ul>li:last-child>a,.sm-dox>li:last-child>ul>li:last-child>:not(ul) a,.sm-dox>li:last-child>ul>li:last-child>ul,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>:not(ul) a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>:not(ul) a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>:not(ul) a,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul { + -moz-border-radius: 0 0 5px 5px; + -webkit-border-radius: 0; + border-radius: 0 0 5px 5px; +} + +.sm-dox>li:last-child>a.highlighted,.sm-dox>li:last-child>:not(ul) a.highlighted,.sm-dox>li:last-child>ul>li:last-child>a.highlighted,.sm-dox>li:last-child>ul>li:last-child>:not(ul) a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>:not(ul) a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>:not(ul) a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>a.highlighted,.sm-dox>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>ul>li:last-child>:not(ul) a.highlighted { + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; +} + +.sm-dox ul { + background: rgba(162,162,162,0.1); +} + +.sm-dox ul a,.sm-dox ul a:focus,.sm-dox ul a:hover,.sm-dox ul a:active { + font-size: 12px; + border-left: 8px solid transparent; + line-height: auto; + text-shadow: none; + background-color: #fff; + background-image: none; +} + +.sm-dox ul a:hover { + background-image: none; + background-repeat: repeat-x; + color: inherit; + text-shadow: 0 1px 1px #000; +} + +.sm-dox ul ul a,.sm-dox ul ul a:hover,.sm-dox ul ul a:focus,.sm-dox ul ul a:active { + border-left: 16px solid transparent; +} + +.sm-dox ul ul ul a,.sm-dox ul ul ul a:hover,.sm-dox ul ul ul a:focus,.sm-dox ul ul ul a:active { + border-left: 24px solid transparent; +} + +.sm-dox ul ul ul ul a,.sm-dox ul ul ul ul a:hover,.sm-dox ul ul ul ul a:focus,.sm-dox ul ul ul ul a:active { + border-left: 32px solid transparent; +} + +.sm-dox ul ul ul ul ul a,.sm-dox ul ul ul ul ul a:hover,.sm-dox ul ul ul ul ul a:focus,.sm-dox ul ul ul ul ul a:active { + border-left: 40px solid transparent; +} + +@media(min-width:768px) { + .sm-dox ul { + position: absolute; + width: 12em; + } + + .sm-dox li { + float: left; + } + + .sm-dox.sm-rtl li { + float: right; + } + + .sm-dox ul li,.sm-dox.sm-rtl ul li,.sm-dox.sm-vertical li { + float: none; + } + + .sm-dox a { + white-space: nowrap; + } + + .sm-dox ul a,.sm-dox.sm-vertical a { + white-space: normal; + } + + .sm-dox .sm-nowrap>li>a,.sm-dox .sm-nowrap>li>:not(ul) a { + white-space: nowrap; + } + + .sm-dox { + padding: 0 10px; + background-image: none; + background-color: #000; + line-height: normal; + background-image: none; + background-color: #333; + background: linear-gradient(to bottom, #333 0%, #111 100%); + color: #ddd; + } + + .sm-dox a span.sub-arrow { + top: 50%; + margin-top: -2px; + right: 12px; + width: 0; + height: 0; + border-width: 4px; + border-style: solid dashed dashed; + border-color: #ddd transparent transparent; + background: transparent; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + } + + .sm-dox a,.sm-dox a:focus,.sm-dox a:active,.sm-dox a:hover,.sm-dox a.highlighted { + padding: 0 1em 0 0; + background-image: none; + background-repeat: no-repeat; + background-position: right; + -moz-border-radius: 0 !important; + -webkit-border-radius: 0; + border-radius: 0 !important; + } + + .sm-dox a:hover { + background-image: none; + background-repeat: repeat-x; + color: #fff; + text-shadow: 0 1px 1px #000; + } + + .sm-dox a:hover span.sub-arrow { + border-color: #fff transparent transparent; + } + + .sm-dox a.has-submenu { + padding-right: 24px; + } + + .sm-dox li { + border-top: 0; + } + + .sm-dox>li>ul:before,.sm-dox>li>ul:after { + content: ''; + position: absolute; + top: -18px; + left: 30px; + width: 0; + height: 0; + overflow: hidden; + border-width: 9px; + border-style: dashed dashed solid; + border-color: transparent transparent #bbb; + } + + .sm-dox>li>ul:after { + top: -16px; + left: 31px; + border-width: 8px; + border-color: transparent transparent #fff; + } + + .sm-dox ul { + border: 1px solid #bbb; + padding: 5px 0; + background: initial; + -moz-border-radius: 5px !important; + -webkit-border-radius: 5px; + border-radius: 5px !important; + -moz-box-shadow: 0 5px 9px rgba(0,0,0,0.2); + -webkit-box-shadow: 0 5px 9px rgba(0,0,0,0.2); + box-shadow: 0 5px 9px rgba(0,0,0,0.2); + } + + .sm-dox ul a span.sub-arrow { + right: 8px; + top: 50%; + margin-top: -5px; + border-width: 5px; + border-color: transparent transparent transparent #555; + border-style: dashed dashed dashed solid; + } + + .sm-dox ul a,.sm-dox ul a:hover,.sm-dox ul a:focus,.sm-dox ul a:active,.sm-dox ul a.highlighted { + color: #555; + background-image: none; + border: 0 !important; + color: #555; + background-image: none; + } + + .sm-dox ul a:hover { + background-image: none; + background-repeat: repeat-x; + color: #fff; + text-shadow: 0 1px 1px #000; + } + + .sm-dox ul a:hover span.sub-arrow { + border-color: transparent transparent transparent #fff; + } + + .sm-dox span.scroll-up,.sm-dox span.scroll-down { + position: absolute; + display: none; + visibility: hidden; + overflow: hidden; + background: initial; + height: 36px; + } + + .sm-dox span.scroll-up:hover,.sm-dox span.scroll-down:hover { + background: #eee; + } + + .sm-dox span.scroll-up:hover span.scroll-up-arrow,.sm-dox span.scroll-up:hover span.scroll-down-arrow { + border-color: transparent transparent #d23600; + } + + .sm-dox span.scroll-down:hover span.scroll-down-arrow { + border-color: #d23600 transparent transparent; + } + + .sm-dox span.scroll-up-arrow,.sm-dox span.scroll-down-arrow { + position: absolute; + top: 0; + left: 50%; + margin-left: -6px; + width: 0; + height: 0; + overflow: hidden; + border-width: 6px; + border-style: dashed dashed solid; + border-color: transparent transparent #555; + } + + .sm-dox span.scroll-down-arrow { + top: 8px; + border-style: solid dashed dashed; + border-color: #555 transparent transparent; + } + + .sm-dox.sm-rtl a.has-submenu { + padding-right: 12px; + padding-left: 24px; + } + + .sm-dox.sm-rtl a span.sub-arrow { + right: auto; + left: 12px; + } + + .sm-dox.sm-rtl.sm-vertical a.has-submenu { + padding: 10px 20px; + } + + .sm-dox.sm-rtl.sm-vertical a span.sub-arrow { + right: auto; + left: 8px; + border-style: dashed solid dashed dashed; + border-color: transparent #555 transparent transparent; + } + + .sm-dox.sm-rtl>li>ul:before { + left: auto; + right: 30px; + } + + .sm-dox.sm-rtl>li>ul:after { + left: auto; + right: 31px; + } + + .sm-dox.sm-rtl ul a.has-submenu { + padding: 10px 20px !important; + } + + .sm-dox.sm-rtl ul a span.sub-arrow { + right: auto; + left: 8px; + border-style: dashed solid dashed dashed; + border-color: transparent #555 transparent transparent; + } + + .sm-dox.sm-vertical { + padding: 10px 0; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + } + + .sm-dox.sm-vertical a { + padding: 10px 20px; + } + + .sm-dox.sm-vertical a:hover,.sm-dox.sm-vertical a:focus,.sm-dox.sm-vertical a:active,.sm-dox.sm-vertical a.highlighted { + background: initial; + } + + .sm-dox.sm-vertical a.disabled { + background-image: none; + } + + .sm-dox.sm-vertical a span.sub-arrow { + right: 8px; + top: 50%; + margin-top: -5px; + border-width: 5px; + border-style: dashed dashed dashed solid; + border-color: transparent transparent transparent #555; + } + + .sm-dox.sm-vertical>li>ul:before,.sm-dox.sm-vertical>li>ul:after { + display: none; + } + + .sm-dox.sm-vertical ul a { + padding: 10px 20px; + } + + .sm-dox.sm-vertical ul a:hover,.sm-dox.sm-vertical ul a:focus,.sm-dox.sm-vertical ul a:active,.sm-dox.sm-vertical ul a.highlighted { + background: #eee; + } + + .sm-dox.sm-vertical ul a.disabled { + background: initial; + } +} diff --git a/icons/128x128/ingen.png b/icons/128x128/ingen.png Binary files differnew file mode 100644 index 00000000..4916acbf --- /dev/null +++ b/icons/128x128/ingen.png diff --git a/icons/16x16/ingen.png b/icons/16x16/ingen.png Binary files differnew file mode 100644 index 00000000..54a691f9 --- /dev/null +++ b/icons/16x16/ingen.png diff --git a/icons/22x22/ingen.png b/icons/22x22/ingen.png Binary files differnew file mode 100644 index 00000000..a0b9431b --- /dev/null +++ b/icons/22x22/ingen.png diff --git a/icons/24x24/ingen.png b/icons/24x24/ingen.png Binary files differnew file mode 100644 index 00000000..7fb4e58f --- /dev/null +++ b/icons/24x24/ingen.png diff --git a/icons/256x256/ingen.png b/icons/256x256/ingen.png Binary files differnew file mode 100644 index 00000000..7fa89cf9 --- /dev/null +++ b/icons/256x256/ingen.png diff --git a/icons/32x32/ingen.png b/icons/32x32/ingen.png Binary files differnew file mode 100644 index 00000000..b426086a --- /dev/null +++ b/icons/32x32/ingen.png diff --git a/icons/48x48/ingen.png b/icons/48x48/ingen.png Binary files differnew file mode 100644 index 00000000..422bcbae --- /dev/null +++ b/icons/48x48/ingen.png diff --git a/icons/64x64/ingen.png b/icons/64x64/ingen.png Binary files differnew file mode 100644 index 00000000..235ff27f --- /dev/null +++ b/icons/64x64/ingen.png diff --git a/icons/ingen_icons.svg b/icons/ingen_icons.svg new file mode 100644 index 00000000..b3fe999d --- /dev/null +++ b/icons/ingen_icons.svg @@ -0,0 +1,7987 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + inkscape:export-ydpi="90.000000" + inkscape:export-xdpi="90.000000" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_set_04.png" + width="311" + height="256" + id="svg11300" + sodipodi:version="0.32" + inkscape:version="0.48.5 r10040" + sodipodi:docname="ingen_icons.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + version="1.0" + style="display:inline;enable-background:new"> + <defs + id="defs3"> + <linearGradient + id="linearGradient9874"> + <stop + style="stop-color:#002b07;stop-opacity:1;" + offset="0" + id="stop9876" /> + <stop + style="stop-color:#002b07;stop-opacity:1;" + offset="1" + id="stop9878" /> + </linearGradient> + <linearGradient + id="linearGradient3771"> + <stop + style="stop-color:#4786e2;stop-opacity:1;" + offset="0" + id="stop3773" /> + <stop + id="stop3775" + offset="0.5" + style="stop-color:#66c7ff;stop-opacity:1;" /> + <stop + style="stop-color:#73adff;stop-opacity:1;" + offset="1" + id="stop3777" /> + </linearGradient> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 128 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="311 : 128 : 1" + inkscape:persp3d-origin="155.5 : 85.333333 : 1" + id="perspective2935" /> + <linearGradient + id="linearGradient3784"> + <stop + id="stop3786" + offset="0" + style="stop-color:#ebffff;stop-opacity:1;" /> + <stop + style="stop-color:#2ea7fb;stop-opacity:1;" + offset="0.30719113" + id="stop3788" /> + <stop + id="stop3790" + offset="1" + style="stop-color:#87c4ff;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3776"> + <stop + style="stop-color:#ebffff;stop-opacity:1;" + offset="0" + id="stop3778" /> + <stop + id="stop3780" + offset="0.29804102" + style="stop-color:#2ea7fb;stop-opacity:1;" /> + <stop + style="stop-color:#4e86ff;stop-opacity:1;" + offset="1" + id="stop3782" /> + </linearGradient> + <linearGradient + id="linearGradient3768"> + <stop + id="stop3770" + offset="0" + style="stop-color:#ebffff;stop-opacity:1;" /> + <stop + style="stop-color:#2ea7fb;stop-opacity:1;" + offset="0.16622767" + id="stop3772" /> + <stop + id="stop3774" + offset="1" + style="stop-color:#0000c8;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3698"> + <stop + style="stop-color:#ebffff;stop-opacity:1;" + offset="0" + id="stop3700" /> + <stop + id="stop3702" + offset="0.22617246" + style="stop-color:#2ea7fb;stop-opacity:1;" /> + <stop + style="stop-color:#2662ff;stop-opacity:1;" + offset="1" + id="stop3704" /> + </linearGradient> + <linearGradient + id="linearGradient3690"> + <stop + id="stop3692" + offset="0" + style="stop-color:#ebffff;stop-opacity:1;" /> + <stop + style="stop-color:#2ea7fb;stop-opacity:1;" + offset="0.15422586" + id="stop3694" /> + <stop + id="stop3696" + offset="1" + style="stop-color:#0000c8;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3676"> + <stop + id="stop3678" + offset="0" + style="stop-color:#0032d6;stop-opacity:1;" /> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0.23738661" + id="stop3680" /> + <stop + style="stop-color:#070f13;stop-opacity:1;" + offset="0.79761904" + id="stop3682" /> + <stop + id="stop3684" + offset="1" + style="stop-color:#0e6ea2;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3668"> + <stop + style="stop-color:#ebffff;stop-opacity:1;" + offset="0" + id="stop3670" /> + <stop + id="stop3672" + offset="0.23382138" + style="stop-color:#2ea7fb;stop-opacity:1;" /> + <stop + style="stop-color:#0f76ff;stop-opacity:1;" + offset="1" + id="stop3674" /> + </linearGradient> + <linearGradient + id="linearGradient3660"> + <stop + id="stop3662" + offset="0" + style="stop-color:#ebffff;stop-opacity:1;" /> + <stop + style="stop-color:#2ea7fb;stop-opacity:1;" + offset="0.18031031" + id="stop3664" /> + <stop + id="stop3666" + offset="1" + style="stop-color:#0000c8;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3635"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop3637" /> + <stop + id="stop3643" + offset="0.28766796" + style="stop-color:#29c9ff;stop-opacity:0;" /> + <stop + style="stop-color:#3ae4ff;stop-opacity:0.33333334;" + offset="0.70321494" + id="stop3645" /> + <stop + style="stop-color:#003bb1;stop-opacity:0.35294119;" + offset="1" + id="stop3639" /> + </linearGradient> + <linearGradient + id="linearGradient3621"> + <stop + style="stop-color:#000b24;stop-opacity:1;" + offset="0" + id="stop3627" /> + <stop + id="stop3629" + offset="1" + style="stop-color:#000000;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3611"> + <stop + id="stop3613" + offset="0" + style="stop-color:#c0feff;stop-opacity:1;" /> + <stop + style="stop-color:#2ea7fb;stop-opacity:1;" + offset="0.11465497" + id="stop3615" /> + <stop + id="stop3617" + offset="1" + style="stop-color:#126fff;stop-opacity:1;" /> + </linearGradient> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 128 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="311 : 128 : 1" + inkscape:persp3d-origin="155.5 : 85.333333 : 1" + id="perspective2837" /> + <linearGradient + id="linearGradient3404"> + <stop + id="stop3406" + offset="0" + style="stop-color:#236eda;stop-opacity:1;" /> + <stop + style="stop-color:#2dabf5;stop-opacity:1;" + offset="0.5" + id="stop3408" /> + <stop + id="stop3411" + offset="1" + style="stop-color:#347feb;stop-opacity:1;" /> + </linearGradient> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 128 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="311 : 128 : 1" + inkscape:persp3d-origin="155.5 : 85.333333 : 1" + id="perspective2632" /> + <linearGradient + id="linearGradient3834"> + <stop + id="stop3836" + offset="0" + style="stop-color:#060607;stop-opacity:1;" /> + <stop + id="stop3838" + offset="1" + style="stop-color:#34464d;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3454"> + <stop + style="stop-color:#2e6cc5;stop-opacity:1;" + offset="0" + id="stop3456" /> + <stop + id="stop3462" + offset="0.5" + style="stop-color:#1596e2;stop-opacity:1;" /> + <stop + style="stop-color:#2b66ba;stop-opacity:1;" + offset="1" + id="stop3458" /> + </linearGradient> + <linearGradient + id="linearGradient3408"> + <stop + style="stop-color:#62deff;stop-opacity:1;" + offset="0" + id="stop3410" /> + <stop + style="stop-color:#2d90ff;stop-opacity:1;" + offset="1" + id="stop3412" /> + </linearGradient> + <linearGradient + id="linearGradient3392"> + <stop + style="stop-color:#3b71bf;stop-opacity:1;" + offset="0" + id="stop3394" /> + <stop + id="stop3400" + offset="0.5" + style="stop-color:#265eaf;stop-opacity:1;" /> + <stop + style="stop-color:#24559d;stop-opacity:1;" + offset="1" + id="stop3396" /> + </linearGradient> + <inkscape:perspective + sodipodi:type="inkscape:persp3d" + inkscape:vp_x="0 : 128 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="311 : 128 : 1" + inkscape:persp3d-origin="155.5 : 85.333333 : 1" + id="perspective3377" /> + <linearGradient + id="linearGradient3372"> + <stop + style="stop-color:#222b31;stop-opacity:1;" + offset="0" + id="stop3374" /> + <stop + style="stop-color:#384750;stop-opacity:0.68627453;" + offset="1" + id="stop3376" /> + </linearGradient> + <linearGradient + id="linearGradient3425"> + <stop + style="stop-color:#ebffff;stop-opacity:1;" + offset="0" + id="stop3427" /> + <stop + id="stop3609" + offset="0.8214286" + style="stop-color:#2ea7fb;stop-opacity:1;" /> + <stop + style="stop-color:#0000c8;stop-opacity:1;" + offset="1" + id="stop3429" /> + </linearGradient> + <linearGradient + id="linearGradient3415"> + <stop + style="stop-color:#3d464b;stop-opacity:1;" + offset="0" + id="stop3417" /> + <stop + style="stop-color:#828788;stop-opacity:1;" + offset="1" + id="stop3419" /> + </linearGradient> + <linearGradient + id="linearGradient3399"> + <stop + style="stop-color:#0000a0;stop-opacity:1;" + offset="0" + id="stop3401" /> + <stop + id="stop3619" + offset="0.23738661" + style="stop-color:#000000;stop-opacity:1;" /> + <stop + id="stop3407" + offset="0.79761904" + style="stop-color:#070f13;stop-opacity:1;" /> + <stop + style="stop-color:#144560;stop-opacity:1;" + offset="1" + id="stop3403" /> + </linearGradient> + <linearGradient + id="linearGradient3371"> + <stop + style="stop-color:#55d4ff;stop-opacity:1;" + offset="0" + id="stop3373" /> + <stop + style="stop-color:#1f3aff;stop-opacity:0.56209153;" + offset="1" + id="stop3375" /> + </linearGradient> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3371" + id="radialGradient3377" + cx="132" + cy="167.88092" + fx="132" + fy="167.88092" + r="111" + gradientTransform="matrix(2.7849536,0,0,0.909328,-242.61387,-38.723414)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3399" + id="linearGradient3411" + gradientUnits="userSpaceOnUse" + x1="105.5" + y1="7.5" + x2="105.5" + y2="256.62848" + gradientTransform="translate(-264,0)" /> + <inkscape:perspective + id="perspective3386" + inkscape:persp3d-origin="372.04724 : 350.78739 : 1" + inkscape:vp_z="744.09448 : 526.18109 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_x="0 : 526.18109 : 1" + sodipodi:type="inkscape:persp3d" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient3460" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3372" + id="linearGradient3534" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.184739,0,0,0.184739,-7.6144648,270.61447)" + x1="-256.5" + y1="132" + x2="-7.5" + y2="132" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient3544" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient3602" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3371" + id="radialGradient3629" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.503543,0,0,0.1644141,226.54296,0.4080921)" + cx="132" + cy="167.88092" + fx="132" + fy="167.88092" + r="111" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3834" + id="linearGradient3647" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.1807229,0,0,0.1807229,-6.1445741,269.14457)" + x1="-257" + y1="132" + x2="-7" + y2="132" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient3707" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3372" + id="linearGradient3842" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.1767069,0,0,0.1767069,-8.6747054,271.67471)" + x1="-256.5" + y1="132" + x2="-7.5" + y2="132" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient3906" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient3969" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient4007" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient3371" + id="radialGradient4028" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.3150401,0,0,0.1030769,243.91472,97.080213)" + cx="132" + cy="167.88092" + fx="132" + fy="167.88092" + r="111" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3834" + id="linearGradient4050" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.120482,0,0,0.120482,-99.596389,269.59636)" + x1="-257" + y1="132" + x2="-7" + y2="132" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3372" + id="linearGradient4062" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.1244981,0,0,0.1244981,-100.06627,270.06627)" + x1="-256.5" + y1="132" + x2="-7.5" + y2="132" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient4246" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient4302" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient4501" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient4557" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3784" + id="linearGradient3461" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(5.263157e-2,0,0,5.2631523e-2,-248.05263,271.05264)" + x1="-8.0000153" + y1="109.99998" + x2="-254.3206" + y2="142.02679" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3404" + id="linearGradient3615" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635-2" + id="linearGradient3658" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.1781377,0,0,0.1781377,-7.7327911,270.73281)" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635-2" + id="linearGradient3688" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.1174089,0,0,0.1174089,-100.82388,270.82389)" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3399" + id="linearGradient3813" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(6.02695e-2,0,0,6.0269496e-2,290.04797,247.048)" + x1="105.5" + y1="7.5" + x2="105.5" + y2="256.62848" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3784" + id="linearGradient3815" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(5.263157e-2,0,0,5.2631523e-2,-248.05263,291.05264)" + x1="-8.0000153" + y1="109.99998" + x2="-254.3206" + y2="142.02679" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635-2" + id="linearGradient3819" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.08097165,0,0,0.08097165,-178.87855,271.87853)" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" /> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath3744"> + <rect + style="opacity:1;fill:#5d91dc;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3746" + width="249" + height="249" + x="-299.5" + y="0.5" + inkscape:label="scalable" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="11.671875" + ry="11.671875" /> + </clipPath> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient3817" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient3821" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408" + id="linearGradient3823" + gradientUnits="userSpaceOnUse" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient3825" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient3827" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408" + id="linearGradient3829" + gradientUnits="userSpaceOnUse" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient3831" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient3833" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408" + id="linearGradient3835" + gradientUnits="userSpaceOnUse" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635" + id="linearGradient3837" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(8.0971655e-2,0,0,8.0971655e-2,-178.87855,311.87853)" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3776" + id="linearGradient3839" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(7.6923126e-2,0,0,7.6922995e-2,-178.84612,311.84616)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7726"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7728" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7730" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7732" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7734" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7736"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7738" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7740" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7742" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7744" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7746"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7748" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7750" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7752" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7754" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7756"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7758" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7760" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7762" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7764" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7766"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7768" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7770" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7772" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7774" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7776"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7778" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7780" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7782" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7784" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7786"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7788" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7790" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7792" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7794" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7796"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7798" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7800" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7802" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7804" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7806"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7808" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7810" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7812" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7814" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7816"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7818" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7820" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7822" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7824" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7826"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7828" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7830" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7832" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7834" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7836"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7838" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7840" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7842" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7844" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7846"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7848" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7850" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7852" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7854" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7856"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7858" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7860" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7862" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7864" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7876"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7878" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7880" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7882" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7884" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7886"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7888" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7890" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7892" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7894" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7896"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7898" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7900" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7902" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7904" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7906"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7908" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7910" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7912" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7914" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7916"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7918" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7920" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7922" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7924" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7926"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7928" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7930" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7932" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7934" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7936"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7938" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7940" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7942" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7944" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7946"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7948" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7950" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7952" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7954" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7956"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7958" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7960" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7962" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7964" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7966"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7968" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7970" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7972" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7974" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7976"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7978" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7980" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7982" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7984" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7986"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7988" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7990" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7992" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7994" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7996"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7998" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8000" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8002" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8004" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8006"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8008" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8010" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8012" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8014" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8016"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8018" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8020" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8022" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8024" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8026"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8028" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8030" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8032" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8034" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8036"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8038" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8040" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8042" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8044" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8046"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8048" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8050" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8052" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8054" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8056"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8058" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8060" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8062" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8064" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8066"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8068" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8070" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8072" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8074" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8076"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8078" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8080" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8082" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8084" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8086"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8088" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8090" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8092" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8094" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8096"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8098" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8100" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8102" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8104" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8106"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8108" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8110" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8112" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8114" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8116"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8118" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8120" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8122" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8124" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8126"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8128" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8130" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8132" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8134" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8136"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8138" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8140" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8142" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8144" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8146"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8148" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8150" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8152" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8154" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8156"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8158" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8160" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8162" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8164" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8166"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8168" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8170" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8172" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8174" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8176"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8178" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8180" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8182" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8184" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8186"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8188" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8190" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8192" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8194" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8196"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8198" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8200" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8202" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8204" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8206"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8208" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8210" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8212" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8214" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8216"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8218" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8220" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8222" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8224" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8226"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8228" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8230" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8232" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8234" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8236"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8238" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8240" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8242" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8244" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8246"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8248" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8250" /> + <feComposite + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8252" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8254" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3611-5" + id="linearGradient3654-3" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0202725,0,0,1.0202725,7.593656,-8.561507)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <linearGradient + id="linearGradient3611-5"> + <stop + id="stop3613-0" + offset="0" + style="stop-color:#38ff7a;stop-opacity:1;" /> + <stop + style="stop-color:#04c945;stop-opacity:1;" + offset="0.11465497" + id="stop3615-3" /> + <stop + id="stop3617-2" + offset="1" + style="stop-color:#008b2e;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3635-2"> + <stop + style="stop-color:#c0c0c0;stop-opacity:1;" + offset="0" + id="stop3637-5" /> + <stop + id="stop3643-3" + offset="0.47374481" + style="stop-color:#00bd56;stop-opacity:0;" /> + <stop + style="stop-color:#71ff95;stop-opacity:0.33333334;" + offset="0.70321494" + id="stop3645-2" /> + <stop + style="stop-color:#008441;stop-opacity:0.35294119;" + offset="1" + id="stop3639-5" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6" + id="linearGradient3557-6" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop3410-8" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6" + id="linearGradient3553-9" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient8282"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop8284" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop8286" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-1" + id="linearGradient4378-0" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,136.35445,-80.78968)" /> + <linearGradient + id="linearGradient3392-1"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop3394-5" /> + <stop + id="stop3400-5" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop3396-8" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-1" + id="linearGradient4381-0" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,136.35445,-80.78968)" /> + <linearGradient + id="linearGradient8294"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop8296" /> + <stop + id="stop8298" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop8300" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-1" + id="linearGradient4358-1" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,136.35445,64.618319)" /> + <linearGradient + id="linearGradient8303"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop8305" /> + <stop + id="stop8307" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop8309" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-1" + id="linearGradient4361-7" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,136.35445,64.618319)" /> + <linearGradient + id="linearGradient8312"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop8314" /> + <stop + id="stop8316" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop8318" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6" + id="linearGradient4366-6" + gradientUnits="userSpaceOnUse" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" + gradientTransform="matrix(1.0093714,0,0,1.0093714,-8.168518,-6.16202)" /> + <linearGradient + id="linearGradient8321"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop8323" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop8325" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-1" + id="linearGradient4369-3" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,-9.053551,-8.085676)" /> + <linearGradient + id="linearGradient8328"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop8330" /> + <stop + id="stop8332" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop8334" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-1" + id="linearGradient4372-3" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,-9.053551,-8.085676)" /> + <linearGradient + id="linearGradient8337"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop8339" /> + <stop + id="stop8341" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop8343" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3415-8" + id="linearGradient3421-8" + x1="-257" + y1="132" + x2="-7" + y2="132" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.024,0,0,1.024,8.085676,-9.053551)" /> + <linearGradient + id="linearGradient3415-8"> + <stop + style="stop-color:#092513;stop-opacity:1;" + offset="0" + id="stop3417-3" /> + <stop + style="stop-color:#145c2d;stop-opacity:1;" + offset="1" + id="stop3419-8" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6" + id="linearGradient3460-6" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3454-6"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop3456-4" /> + <stop + id="stop3462-7" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop3458-1" /> + </linearGradient> + <linearGradient + id="linearGradient8355"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop8357" /> + <stop + id="stop8359" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop8361" /> + </linearGradient> + <linearGradient + id="linearGradient8364"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop8366" /> + <stop + id="stop8368" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop8370" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3621-1" + id="linearGradient3405-8" + x1="105.5" + y1="7.5" + x2="105.5" + y2="256.62848" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.024,0,0,1.024,-10.168,-10.168)" /> + <linearGradient + id="linearGradient3621-1"> + <stop + style="stop-color:#000b24;stop-opacity:1;" + offset="0" + id="stop3627-9" /> + <stop + id="stop3629-4" + offset="1" + style="stop-color:#000000;stop-opacity:1;" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3621-1" + id="linearGradient8776" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.024,0,0,1.024,-9.053551,-8.085676)" + x1="105.5" + y1="7.5" + x2="105.5" + y2="256.62848" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635-2" + id="linearGradient8778" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.0381489,1.0401821,0,-3.4526636,-2.5827515)" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6" + id="linearGradient8780" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6" + id="linearGradient8782" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient3611-5-6"> + <stop + id="stop3613-0-0" + offset="0" + style="stop-color:#38ff7a;stop-opacity:1;" /> + <stop + style="stop-color:#04c945;stop-opacity:1;" + offset="0.11465497" + id="stop3615-3-4" /> + <stop + id="stop3617-2-7" + offset="1" + style="stop-color:#008b2e;stop-opacity:1;" /> + </linearGradient> + <linearGradient + y2="160.49998" + x2="-255.99998" + y1="109.99998" + x1="-8.0000153" + gradientTransform="matrix(1.0202725,0,0,1.0202725,7.6759858,-7.6759527)" + gradientUnits="userSpaceOnUse" + id="linearGradient8800" + xlink:href="#linearGradient3611-5-6" + inkscape:collect="always" /> + <linearGradient + id="linearGradient3611-5-6-7"> + <stop + id="stop3613-0-0-0" + offset="0" + style="stop-color:#38ff7a;stop-opacity:1;" /> + <stop + style="stop-color:#04c945;stop-opacity:1;" + offset="0.11465497" + id="stop3615-3-4-9" /> + <stop + id="stop3617-2-7-5" + offset="1" + style="stop-color:#008b2e;stop-opacity:1;" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7976-8"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7978-1" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7980-8" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7982-7" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7984-8" /> + </filter> + <linearGradient + y2="160.49998" + x2="-255.99998" + y1="109.99998" + x1="-8.0000153" + gradientTransform="matrix(1.0202725,0,0,1.0202725,7.593656,-8.561507)" + gradientUnits="userSpaceOnUse" + id="linearGradient9349" + xlink:href="#linearGradient3611-5-6-7" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3611-5-6-8" + id="linearGradient3654-3-6-2" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0202725,0,0,1.0202725,7.593656,-8.561507)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <linearGradient + id="linearGradient3611-5-6-8"> + <stop + id="stop3613-0-0-1" + offset="0" + style="stop-color:#38ff7a;stop-opacity:1;" /> + <stop + style="stop-color:#04c945;stop-opacity:1;" + offset="0.11465497" + id="stop3615-3-4-5" /> + <stop + id="stop3617-2-7-8" + offset="1" + style="stop-color:#008b2e;stop-opacity:1;" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7976-7"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7978-3" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7980-4" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7982-6" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7984-6" /> + </filter> + <linearGradient + y2="160.49998" + x2="-255.99998" + y1="109.99998" + x1="-8.0000153" + gradientTransform="matrix(1.0202725,0,0,1.0202725,7.593656,-8.561507)" + gradientUnits="userSpaceOnUse" + id="linearGradient9349-5" + xlink:href="#linearGradient3611-5-6-8" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3611-5-6-9" + id="linearGradient3654-3-6-5" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0202725,0,0,1.0202725,7.593656,-8.561507)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <linearGradient + id="linearGradient3611-5-6-9"> + <stop + id="stop3613-0-0-6" + offset="0" + style="stop-color:#38ff7a;stop-opacity:1;" /> + <stop + style="stop-color:#04c945;stop-opacity:1;" + offset="0.11465497" + id="stop3615-3-4-4" /> + <stop + id="stop3617-2-7-1" + offset="1" + style="stop-color:#008b2e;stop-opacity:1;" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7976-2"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7978-18" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7980-5" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7982-0" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7984-86" /> + </filter> + <linearGradient + y2="160.49998" + x2="-255.99998" + y1="109.99998" + x1="-8.0000153" + gradientTransform="matrix(1.0202725,0,0,1.0202725,7.593656,-8.561507)" + gradientUnits="userSpaceOnUse" + id="linearGradient9349-58" + xlink:href="#linearGradient3611-5-6-9" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-5" + id="linearGradient3460-6-3" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3454-6-5"> + <stop + style="stop-color:#239634;stop-opacity:1;" + offset="0" + id="stop3456-4-8" /> + <stop + id="stop3462-7-6" + offset="0.5" + style="stop-color:#0c7f1c;stop-opacity:1;" /> + <stop + style="stop-color:#259835;stop-opacity:1;" + offset="1" + id="stop3458-1-9" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-5" + id="linearGradient8780-8" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient9451"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop9453" /> + <stop + id="stop9455" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop9457" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-5" + id="linearGradient8782-9" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient9460"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop9462" /> + <stop + id="stop9464" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop9466" /> + </linearGradient> + <linearGradient + y2="131.97285" + x2="169.44019" + y1="131.97285" + x1="93.869286" + gradientUnits="userSpaceOnUse" + id="linearGradient9471" + xlink:href="#linearGradient3454-6-5" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-1" + id="linearGradient3460-6-8" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3454-6-1"> + <stop + style="stop-color:#1f862e;stop-opacity:1;" + offset="0" + id="stop3456-4-1" /> + <stop + id="stop3462-7-5" + offset="0.5" + style="stop-color:#0e9120;stop-opacity:1;" /> + <stop + style="stop-color:#28a93b;stop-opacity:1;" + offset="1" + id="stop3458-1-1" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-1" + id="linearGradient8780-0" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient9451-4"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop9453-9" /> + <stop + id="stop9455-9" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop9457-0" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-1" + id="linearGradient8782-8" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient9460-8"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop9462-1" /> + <stop + id="stop9464-9" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop9466-4" /> + </linearGradient> + <linearGradient + y2="131.97285" + x2="169.44019" + y1="131.97285" + x1="93.869286" + gradientUnits="userSpaceOnUse" + id="linearGradient9471-1" + xlink:href="#linearGradient3454-6-1" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-7" + id="linearGradient3460-6-4" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3454-6-7"> + <stop + style="stop-color:#1d7e2b;stop-opacity:1;" + offset="0" + id="stop3456-4-5" /> + <stop + id="stop3462-7-4" + offset="0.5" + style="stop-color:#096416;stop-opacity:1;" /> + <stop + style="stop-color:#1e812d;stop-opacity:1;" + offset="1" + id="stop3458-1-4" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-7" + id="linearGradient8780-7" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient9451-5"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop9453-97" /> + <stop + id="stop9455-5" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop9457-1" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-7" + id="linearGradient8782-5" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient9460-4"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop9462-4" /> + <stop + id="stop9464-99" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop9466-8" /> + </linearGradient> + <linearGradient + y2="131.97285" + x2="169.44019" + y1="131.97285" + x1="93.869286" + gradientUnits="userSpaceOnUse" + id="linearGradient9471-9" + xlink:href="#linearGradient3454-6-7" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient3557-6-2" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6-4"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop3410-8-3" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4-8" /> + </linearGradient> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,-24.714975,-96.380316)" + gradientUnits="userSpaceOnUse" + id="linearGradient9754" + xlink:href="#linearGradient3408-6-4" + inkscape:collect="always" /> + <linearGradient + id="linearGradient3408-6-8"> + <stop + style="stop-color:#116c30;stop-opacity:1;" + offset="0" + id="stop3410-8-34" /> + <stop + style="stop-color:#003c13;stop-opacity:1;" + offset="1" + id="stop3412-4-6" /> + </linearGradient> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,-24.714975,-96.380316)" + gradientUnits="userSpaceOnUse" + id="linearGradient9787" + xlink:href="#linearGradient3408-6-8" + inkscape:collect="always" /> + <linearGradient + id="linearGradient3408-6-84"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop3410-8-8" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4-0" /> + </linearGradient> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,-24.714975,-96.380333)" + gradientUnits="userSpaceOnUse" + id="linearGradient9856" + xlink:href="#linearGradient3408-6-84" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient3553-9-7" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6-6"> + <stop + style="stop-color:#aff2c6;stop-opacity:1;" + offset="0" + id="stop3410-8-6" /> + <stop + style="stop-color:#00842b;stop-opacity:1;" + offset="1" + id="stop3412-4-3" /> + </linearGradient> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,-24.714975,-96.380333)" + gradientUnits="userSpaceOnUse" + id="linearGradient9897" + xlink:href="#linearGradient3408-6-6" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-2" + id="linearGradient3557-6-2-6" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6-4-2"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop3410-8-3-1" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4-8-4" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8016-8"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8018-6" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8020-2" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8022-2" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8024-2" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-2" + id="linearGradient9942" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient9944"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop9946" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop9948" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-2" + id="linearGradient9950" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient9952"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop9954" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop9956" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-2" + id="linearGradient9958" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient9960"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop9962" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop9964" /> + </linearGradient> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9970" + xlink:href="#linearGradient3408-6-4-2" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9972" + xlink:href="#linearGradient3408-6-4-2" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9974" + xlink:href="#linearGradient3408-6-4-2" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9976" + xlink:href="#linearGradient3408-6-4-2" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-3" + id="linearGradient3557-6-2-9" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6-4-3"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop3410-8-3-19" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4-8-3" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8016-6"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8018-9" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8020-5" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8022-1" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8024-6" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-3" + id="linearGradient9942-1" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient9944-9"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop9946-1" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop9948-6" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-3" + id="linearGradient9950-7" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient9952-9"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop9954-5" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop9956-8" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-3" + id="linearGradient9958-7" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient9960-8"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop9962-9" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop9964-1" /> + </linearGradient> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9970-6" + xlink:href="#linearGradient3408-6-4-3" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9972-7" + xlink:href="#linearGradient3408-6-4-3" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9974-5" + xlink:href="#linearGradient3408-6-4-3" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9976-0" + xlink:href="#linearGradient3408-6-4-3" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-8" + id="linearGradient3557-6-2-1" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6-4-8"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop3410-8-3-9" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4-8-5" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8016-7"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8018-2" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8020-3" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8022-9" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8024-7" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-8" + id="linearGradient9942-6" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient9944-5"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop9946-15" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop9948-9" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-8" + id="linearGradient9950-72" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient9952-8"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop9954-52" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop9956-5" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-8" + id="linearGradient9958-5" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient9960-2"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop9962-8" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop9964-3" /> + </linearGradient> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9970-9" + xlink:href="#linearGradient3408-6-4-8" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9972-5" + xlink:href="#linearGradient3408-6-4-8" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9974-3" + xlink:href="#linearGradient3408-6-4-8" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient9976-3" + xlink:href="#linearGradient3408-6-4-8" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3621-2" + id="linearGradient3405" + x1="105.5" + y1="7.5" + x2="105.5" + y2="256.62848" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0232901,0,0,1.0232901,-9.9858987,-9.9858987)" /> + <linearGradient + id="linearGradient3621-2"> + <stop + style="stop-color:#000b24;stop-opacity:1;" + offset="0" + id="stop3627-0" /> + <stop + id="stop3629-42" + offset="1" + style="stop-color:#000000;stop-opacity:1;" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-5" + id="linearGradient3460-5" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3454-5"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop3456-2" /> + <stop + id="stop3462-76" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop3458-5" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-5" + id="linearGradient3051" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient10415"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop10417" /> + <stop + id="stop10419" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop10421" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-5" + id="linearGradient3053" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient10424"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop10426" /> + <stop + id="stop10428" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop10430" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3415-2" + id="linearGradient3421" + x1="-257" + y1="132" + x2="-7" + y2="132" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.024,0,0,1.024,7.668008,-9.167999)" /> + <linearGradient + id="linearGradient3415-2"> + <stop + style="stop-color:#092513;stop-opacity:1;" + offset="0" + id="stop3417-6" /> + <stop + style="stop-color:#145c2d;stop-opacity:1;" + offset="1" + id="stop3419-7" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-3" + id="linearGradient4372" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,-9.167999,-7.668008)" /> + <linearGradient + id="linearGradient3392-3"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop3394-4" /> + <stop + id="stop3400-2" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop3396-2" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-3" + id="linearGradient4369" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,-9.167999,-7.668008)" /> + <linearGradient + id="linearGradient10442"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop10444" /> + <stop + id="stop10446" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop10448" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-0" + id="linearGradient4366" + gradientUnits="userSpaceOnUse" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" + gradientTransform="matrix(1.0093714,0,0,1.0093714,-8.282966,-5.744351)" /> + <linearGradient + id="linearGradient3408-0"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop3410-6" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-5" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-3" + id="linearGradient4361" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,136.24,65.035988)" /> + <linearGradient + id="linearGradient10455"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop10457" /> + <stop + id="stop10459" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop10461" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-3" + id="linearGradient4358" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,136.24,65.035988)" /> + <linearGradient + id="linearGradient10464"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop10466" /> + <stop + id="stop10468" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop10470" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-3" + id="linearGradient4381" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,136.24,-80.37201)" /> + <linearGradient + id="linearGradient10473"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop10475" /> + <stop + id="stop10477" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop10479" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-3" + id="linearGradient4378" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,136.24,-80.37201)" /> + <linearGradient + id="linearGradient10482"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop10484" /> + <stop + id="stop10486" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop10488" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-0" + id="linearGradient3553" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.12503,-78.44835)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient10491"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop10493" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop10495" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-0" + id="linearGradient3557" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.12503,66.959642)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient10498"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop10500" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop10502" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635-7" + id="linearGradient3056" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.0246645,1.0268467,0,-2.517702,-0.74467598)" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" /> + <linearGradient + id="linearGradient3635-7"> + <stop + style="stop-color:#c0c0c0;stop-opacity:1;" + offset="0" + id="stop3637-6" /> + <stop + id="stop3643-0" + offset="0.47374481" + style="stop-color:#00bd56;stop-opacity:0;" /> + <stop + style="stop-color:#71ff95;stop-opacity:0.33333334;" + offset="0.70321494" + id="stop3645-0" /> + <stop + style="stop-color:#008441;stop-opacity:0.35294119;" + offset="1" + id="stop3639-0" /> + </linearGradient> + <linearGradient + id="linearGradient3611-8"> + <stop + id="stop3613-4" + offset="0" + style="stop-color:#38ff7a;stop-opacity:1;" /> + <stop + style="stop-color:#04c945;stop-opacity:1;" + offset="0.11465497" + id="stop3615-8" /> + <stop + id="stop3617-7" + offset="1" + style="stop-color:#008b2e;stop-opacity:1;" /> + </linearGradient> + <linearGradient + y2="256.62848" + x2="105.5" + y1="7.5" + x1="105.5" + gradientTransform="matrix(1.0232901,0,0,1.0232901,-8.985898,-7.485906)" + gradientUnits="userSpaceOnUse" + id="linearGradient10531" + xlink:href="#linearGradient3621-2" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3611-1" + id="linearGradient3654-39" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0202725,0,0,1.0202725,9.6759793,-9.6759565)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <linearGradient + id="linearGradient3611-1"> + <stop + id="stop3613-3" + offset="0" + style="stop-color:#00bd3f;stop-opacity:1;" /> + <stop + style="stop-color:#04c042;stop-opacity:1;" + offset="0.11465497" + id="stop3615-33" /> + <stop + id="stop3617-1" + offset="1" + style="stop-color:#007727;stop-opacity:1;" /> + </linearGradient> + <linearGradient + y2="160.49998" + x2="-255.99998" + y1="109.99998" + x1="-8.0000153" + gradientTransform="matrix(1.0202725,0,0,1.0202725,7.6759879,-7.6759538)" + gradientUnits="userSpaceOnUse" + id="linearGradient10740" + xlink:href="#linearGradient3611-1" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-3" + id="linearGradient10760" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-3" + id="linearGradient10762" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-3" + id="linearGradient10764" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-8" + id="linearGradient10766" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-8" + id="linearGradient10768" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-8" + id="linearGradient10770" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-2" + id="linearGradient10772" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-2" + id="linearGradient10774" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-2" + id="linearGradient10776" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-1" + id="linearGradient10778" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-1" + id="linearGradient10780" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10782" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10784" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10786" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10788" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10790" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10792" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10794" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10796" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10798" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10800" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10802" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-6" + id="linearGradient10804" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,-78.86602)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-5" + id="linearGradient10806" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-5" + id="linearGradient10808" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient3611-5-6-7-0"> + <stop + id="stop3613-0-0-0-8" + offset="0" + style="stop-color:#38ff7a;stop-opacity:1;" /> + <stop + style="stop-color:#04c945;stop-opacity:1;" + offset="0.11465497" + id="stop3615-3-4-9-2" /> + <stop + id="stop3617-2-7-5-6" + offset="1" + style="stop-color:#008b2e;stop-opacity:1;" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7976-8-6"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7978-1-7" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7980-8-6" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7982-7-8" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7984-8-4" /> + </filter> + <linearGradient + id="linearGradient3635-2-5"> + <stop + style="stop-color:#c0c0c0;stop-opacity:1;" + offset="0" + id="stop3637-5-3" /> + <stop + id="stop3643-3-0" + offset="0.47374481" + style="stop-color:#00bd56;stop-opacity:0;" /> + <stop + style="stop-color:#71ff95;stop-opacity:0.33333334;" + offset="0.70321494" + id="stop3645-2-8" /> + <stop + style="stop-color:#008441;stop-opacity:0.35294119;" + offset="1" + id="stop3639-5-6" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7966-7"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7968-9" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7970-6" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7972-9" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7974-3" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-3-5" + id="linearGradient3557-6-2-9-9" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6-4-3-5"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop3410-8-3-19-6" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4-8-3-4" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8016-6-0"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8018-9-2" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8020-5-9" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8022-1-4" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8024-6-6" /> + </filter> + <linearGradient + id="linearGradient4508"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop4510" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop4512" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter4514"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood4516" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend4518" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite4520" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite4522" /> + </filter> + <linearGradient + id="linearGradient4525"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop4527" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop4529" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter4531"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood4533" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend4535" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite4537" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite4539" /> + </filter> + <linearGradient + id="linearGradient4542"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop4544" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop4546" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter4548"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood4550" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend4552" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite4554" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite4556" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-8-1" + id="linearGradient3557-6-2-1-3" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6-4-8-1"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop3410-8-3-9-6" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4-8-5-1" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8016-7-8"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8018-2-2" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8020-3-9" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8022-9-2" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8024-7-5" /> + </filter> + <linearGradient + id="linearGradient4568"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop4570" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop4572" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter4574"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood4576" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend4578" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite4580" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite4582" /> + </filter> + <linearGradient + id="linearGradient4585"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop4587" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop4589" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter4591"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood4593" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend4595" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite4597" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite4599" /> + </filter> + <linearGradient + id="linearGradient4602"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop4604" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop4606" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter4608"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood4610" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend4612" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite4614" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite4616" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-2-5" + id="linearGradient3557-6-2-6-4" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6-4-2-5"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop3410-8-3-1-5" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4-8-4-3" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8016-8-3"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8018-6-4" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8020-2-9" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8022-2-2" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8024-2-0" /> + </filter> + <linearGradient + id="linearGradient4628"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop4630" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop4632" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter4634"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood4636" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend4638" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite4640" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite4642" /> + </filter> + <linearGradient + id="linearGradient4645"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop4647" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop4649" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter4651"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood4653" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend4655" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite4657" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite4659" /> + </filter> + <linearGradient + id="linearGradient4662"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop4664" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop4666" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter4668"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood4670" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend4672" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite4674" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite4676" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-1-8" + id="linearGradient8782-8-0" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient3454-6-1-8"> + <stop + style="stop-color:#1f862e;stop-opacity:1;" + offset="0" + id="stop3456-4-1-8" /> + <stop + id="stop3462-7-5-8" + offset="0.5" + style="stop-color:#0e9120;stop-opacity:1;" /> + <stop + style="stop-color:#28a93b;stop-opacity:1;" + offset="1" + id="stop3458-1-1-2" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8096-5"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8098-6" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8100-7" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8102-8" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8104-3" /> + </filter> + <linearGradient + id="linearGradient4689"> + <stop + style="stop-color:#1f862e;stop-opacity:1;" + offset="0" + id="stop4691" /> + <stop + id="stop4693" + offset="0.5" + style="stop-color:#0e9120;stop-opacity:1;" /> + <stop + style="stop-color:#28a93b;stop-opacity:1;" + offset="1" + id="stop4695" /> + </linearGradient> + <linearGradient + id="linearGradient4698"> + <stop + style="stop-color:#1f862e;stop-opacity:1;" + offset="0" + id="stop4700" /> + <stop + id="stop4702" + offset="0.5" + style="stop-color:#0e9120;stop-opacity:1;" /> + <stop + style="stop-color:#28a93b;stop-opacity:1;" + offset="1" + id="stop4704" /> + </linearGradient> + <linearGradient + id="linearGradient3834-9"> + <stop + id="stop3836-0" + offset="0" + style="stop-color:#060607;stop-opacity:1;" /> + <stop + id="stop3838-3" + offset="1" + style="stop-color:#34464d;stop-opacity:1;" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8106-6"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8108-9" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8110-1" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8112-0" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8114-4" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8116-6"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8118-6" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8120-7" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8122-1" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8124-5" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-2-5" + id="linearGradient5112" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3611-5-6-8-6" + id="linearGradient3654-3-6-2-6" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0202725,0,0,1.0202725,7.593656,-8.561507)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <linearGradient + id="linearGradient3611-5-6-8-6"> + <stop + id="stop3613-0-0-1-0" + offset="0" + style="stop-color:#38ff7a;stop-opacity:1;" /> + <stop + style="stop-color:#04c945;stop-opacity:1;" + offset="0.11465497" + id="stop3615-3-4-5-7" /> + <stop + id="stop3617-2-7-8-7" + offset="1" + style="stop-color:#008b2e;stop-opacity:1;" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7976-7-3"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7978-3-1" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7980-4-6" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7982-6-1" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7984-6-0" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635-2-6" + id="linearGradient3819-7" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.08097165,0,0,0.08097165,-178.87855,271.87853)" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" /> + <linearGradient + id="linearGradient3635-2-6"> + <stop + style="stop-color:#c0c0c0;stop-opacity:1;" + offset="0" + id="stop3637-5-2" /> + <stop + id="stop3643-3-2" + offset="0.47374481" + style="stop-color:#00bd56;stop-opacity:0;" /> + <stop + style="stop-color:#71ff95;stop-opacity:0.33333334;" + offset="0.70321494" + id="stop3645-2-85" /> + <stop + style="stop-color:#008441;stop-opacity:0.35294119;" + offset="1" + id="stop3639-5-7" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7946-1"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7948-7" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7950-2" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7952-1" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7954-2" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient3557-6-2-2" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-6-4-0"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop3410-8-3-7" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-4-8-2" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7996-4"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7998-2" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8000-8" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8002-8" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8004-6" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5156" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5158"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5160" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5162" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5164" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5166"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5168" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5170" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5172" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5174"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5176" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5178" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5180" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5182"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5184" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5186" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8006-6"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8008-4" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8010-9" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8012-3" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8014-4" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5193" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5195"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5197" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5199" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5201" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5203"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5205" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5207" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5209" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5211"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5213" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5215" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5217" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5219"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5221" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5223" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8016-2"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8018-4" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8020-0" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8022-5" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8024-69" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5230" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5232"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5234" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5236" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5238" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5240"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5242" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5244" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4-0" + id="linearGradient5246" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient5248"> + <stop + style="stop-color:#1aa348;stop-opacity:1;" + offset="0" + id="stop5250" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop5252" /> + </linearGradient> + <linearGradient + gradientTransform="matrix(0.9999987,0,0,0.9999987,-8.496575,1.7037808)" + inkscape:collect="always" + xlink:href="#linearGradient3454-6-7-1" + id="linearGradient8782-5-1" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient3454-6-7-1"> + <stop + style="stop-color:#1d7e2b;stop-opacity:1;" + offset="0" + id="stop3456-4-5-1" /> + <stop + id="stop3462-7-4-9" + offset="0.5" + style="stop-color:#096416;stop-opacity:1;" /> + <stop + style="stop-color:#1e812d;stop-opacity:1;" + offset="1" + id="stop3458-1-4-6" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8026-0"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8028-2" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8030-5" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8032-4" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8034-6" /> + </filter> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-7-1" + id="linearGradient5264" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient5266"> + <stop + style="stop-color:#1d7e2b;stop-opacity:1;" + offset="0" + id="stop5268" /> + <stop + id="stop5270" + offset="0.5" + style="stop-color:#096416;stop-opacity:1;" /> + <stop + style="stop-color:#1e812d;stop-opacity:1;" + offset="1" + id="stop5272" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8036-0"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8038-6" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8040-6" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8042-9" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8044-0" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8046-3"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8048-1" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8050-8" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8052-3" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8054-9" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8056-6"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8058-9" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8060-8" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8062-9" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8064-3" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8216-2"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8218-5" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8220-5" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8222-7" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8224-1" /> + </filter> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter8246-6"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood8248-0" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend8250-2" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite8252-7" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite8254-2" /> + </filter> + <linearGradient + gradientTransform="matrix(0.9999987,0,0,0.9999987,-8.496575,1.7037808)" + y2="131.97285" + x2="169.44019" + y1="131.97285" + x1="93.869286" + gradientUnits="userSpaceOnUse" + id="linearGradient5319" + xlink:href="#linearGradient3454-6-7-1" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5321" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5323" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5325" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5327" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5329" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5331" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5333" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5335" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5337" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5339" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + y2="167" + x2="54" + y1="95.999992" + x1="44.5" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + gradientUnits="userSpaceOnUse" + id="linearGradient5341" + xlink:href="#linearGradient3408-6-4-0" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5660" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5662" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5664" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5666" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5668" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5670" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5672" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5674" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5676" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5678" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-6-4" + id="linearGradient5680" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,137.23948,66.541973)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-7" + id="linearGradient5682" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientTransform="translate(0,-38)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-7" + id="linearGradient5684" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientTransform="translate(0,-38)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3611-6" + id="linearGradient3654" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0202725,0,0,1.0202725,8.1759643,-16.675958)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <linearGradient + id="linearGradient3611-6"> + <stop + id="stop3613-9" + offset="0" + style="stop-color:#22bc55;stop-opacity:1;" /> + <stop + style="stop-color:#1a9844;stop-opacity:1;" + offset="0.16842106" + id="stop3615-0" /> + <stop + id="stop3617-9" + offset="1" + style="stop-color:#196834;stop-opacity:1;" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635-1" + id="linearGradient3056-7" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.0189475,1.0211255,0,-9.7696342,-1.0993073)" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" /> + <linearGradient + id="linearGradient3635-1"> + <stop + style="stop-color:#c0c0c0;stop-opacity:1;" + offset="0" + id="stop3637-55" /> + <stop + id="stop3643-8" + offset="0.47374481" + style="stop-color:#00bd56;stop-opacity:0;" /> + <stop + style="stop-color:#71ff95;stop-opacity:0.33333334;" + offset="0.70321494" + id="stop3645-4" /> + <stop + style="stop-color:#008441;stop-opacity:0.35294119;" + offset="1" + id="stop3639-3" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-60" + id="linearGradient3557-7" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,129.12503,65.959664)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3408-60"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop3410-64" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412-6" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-60" + id="linearGradient3553-1" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,129.12503,-79.448334)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + id="linearGradient3845"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop3847" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3849" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-0" + id="linearGradient4378-8" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,128.24,-81.371984)" /> + <linearGradient + id="linearGradient3392-0"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop3394-3" /> + <stop + id="stop3400-29" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop3396-4" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-0" + id="linearGradient4381-4" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,128.24,-81.371984)" /> + <linearGradient + id="linearGradient3857"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop3859" /> + <stop + id="stop3861" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop3863" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-0" + id="linearGradient4358-7" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,128.24,64.03601)" /> + <linearGradient + id="linearGradient3866"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop3868" /> + <stop + id="stop3870" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop3872" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-0" + id="linearGradient4361-5" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,128.24,64.03601)" /> + <linearGradient + id="linearGradient3875"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop3877" /> + <stop + id="stop3879" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop3881" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408-60" + id="linearGradient4366-2" + gradientUnits="userSpaceOnUse" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" + gradientTransform="matrix(1.0093714,0,0,1.0093714,-16.282968,-6.7443343)" /> + <linearGradient + id="linearGradient3884"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop3886" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3888" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-0" + id="linearGradient4369-1" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,-17.168001,-8.6679843)" /> + <linearGradient + id="linearGradient3891"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop3893" /> + <stop + id="stop3895" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop3897" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392-0" + id="linearGradient4372-9" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,-17.168001,-8.6679843)" /> + <linearGradient + id="linearGradient3900"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop3902" /> + <stop + id="stop3904" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop3906" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-7" + id="linearGradient3460-9" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient3454-7"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop3456-8" /> + <stop + id="stop3462-1" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop3458-8" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-7" + id="linearGradient3051-7" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient3914"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop3916" /> + <stop + id="stop3918" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop3920" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-7" + id="linearGradient3053-4" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + id="linearGradient3923"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop3925" /> + <stop + id="stop3927" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop3929" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3621-23" + id="linearGradient3405-3" + x1="105.5" + y1="7.5" + x2="105.5" + y2="256.62848" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0232901,0,0,1.0232901,-9.9858987,-9.9858987)" /> + <linearGradient + id="linearGradient3621-23"> + <stop + style="stop-color:#000b24;stop-opacity:1;" + offset="0" + id="stop3627-7" /> + <stop + id="stop3629-6" + offset="1" + style="stop-color:#000000;stop-opacity:1;" /> + </linearGradient> + <linearGradient + y2="256.62848" + x2="105.5" + y1="7.5" + x1="105.5" + gradientTransform="matrix(1.0232901,0,0,1.0232901,-16.9859,-8.4858843)" + gradientUnits="userSpaceOnUse" + id="linearGradient3950" + xlink:href="#linearGradient3621-23" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3611-6-4" + id="linearGradient3654-2" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0202725,0,0,1.0202725,8.1759643,-16.675958)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <linearGradient + id="linearGradient3611-6-4"> + <stop + id="stop3613-9-8" + offset="0" + style="stop-color:#22bc55;stop-opacity:1;" /> + <stop + style="stop-color:#1a9844;stop-opacity:1;" + offset="0.16842106" + id="stop3615-0-8" /> + <stop + id="stop3617-9-8" + offset="1" + style="stop-color:#196834;stop-opacity:1;" /> + </linearGradient> + <linearGradient + y2="160.49998" + x2="-255.99998" + y1="109.99998" + x1="-8.0000153" + gradientTransform="matrix(1.0202725,0,0,1.0202725,4.6723327,-4.6723159)" + gradientUnits="userSpaceOnUse" + id="linearGradient6523" + xlink:href="#linearGradient3611-6-4" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3611-6-4-9" + id="linearGradient3654-2-1" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0202725,0,0,1.0202725,8.1759643,-16.675958)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <linearGradient + id="linearGradient3611-6-4-9"> + <stop + id="stop3613-9-8-6" + offset="0" + style="stop-color:#22bc55;stop-opacity:1;" /> + <stop + style="stop-color:#1a9844;stop-opacity:1;" + offset="0.16842106" + id="stop3615-0-8-6" /> + <stop + id="stop3617-9-8-1" + offset="1" + style="stop-color:#196834;stop-opacity:1;" /> + </linearGradient> + <filter + x="0" + y="0" + width="1" + height="1" + inkscape:label="Lightness-Contrast" + inkscape:menu="Color" + inkscape:menu-tooltip="Increase or decrease lightness and contrast" + color-interpolation-filters="sRGB" + id="filter7976-6"> + <feFlood + result="result1" + flood-opacity="0" + flood-color="rgb(128,128,128)" + id="feFlood7978-0" /> + <feBlend + mode="normal" + in2="SourceGraphic" + result="result2" + id="feBlend7980-7" /> + <feComposite + k4="0" + k3="0" + k1="0" + k2="1" + operator="arithmetic" + in2="SourceGraphic" + id="feComposite7982-5" /> + <feComposite + operator="in" + in2="SourceGraphic" + id="feComposite7984-60" /> + </filter> + <linearGradient + y2="160.49998" + x2="-255.99998" + y1="109.99998" + x1="-8.0000153" + gradientTransform="matrix(1.0202725,0,0,1.0202725,8.1759643,-16.675958)" + gradientUnits="userSpaceOnUse" + id="linearGradient7072" + xlink:href="#linearGradient3611-6-4-9" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-7" + id="linearGradient7102" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454-6-7" + id="linearGradient7104" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + </defs> + <sodipodi:namedview + stroke="#ef2929" + fill="#f57900" + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="0.25490196" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.0000001" + inkscape:cx="5.8572534" + inkscape:cy="215.36149" + inkscape:current-layer="layer5" + showgrid="false" + inkscape:grid-bbox="true" + inkscape:document-units="px" + inkscape:showpageshadow="false" + inkscape:window-width="2556" + inkscape:window-height="1574" + inkscape:window-x="0" + inkscape:window-y="22" + width="400px" + height="300px" + inkscape:snap-nodes="false" + inkscape:snap-bbox="true" + objecttolerance="7" + gridtolerance="12" + guidetolerance="13" + showborder="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:window-maximized="0"> + <inkscape:grid + type="xygrid" + id="grid5883" + spacingx="1px" + spacingy="1px" + enabled="true" + visible="true" + empspacing="5" /> + </sodipodi:namedview> + <metadata + id="metadata4"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:creator> + <cc:Agent> + <dc:title>Breathe Icon Team</dc:title> + </cc:Agent> + </dc:creator> + <dc:source /> + <cc:license + rdf:resource="http://creativecommons.org/licenses/by-nc-sa/3.0/" /> + <dc:title>Breathe Icon Set template</dc:title> + <dc:subject> + <rdf:Bag /> + </dc:subject> + <dc:date /> + <dc:rights> + <cc:Agent> + <dc:title /> + </cc:Agent> + </dc:rights> + <dc:publisher> + <cc:Agent> + <dc:title /> + </cc:Agent> + </dc:publisher> + <dc:identifier /> + <dc:relation /> + <dc:language /> + <dc:coverage /> + <dc:description /> + <dc:contributor> + <cc:Agent> + <dc:title>Jakub Steiner, Cory Kontros</dc:title> + </cc:Agent> + </dc:contributor> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/by-nc-sa/3.0/"> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Notice" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Attribution" /> + <cc:prohibits + rdf:resource="http://creativecommons.org/ns#CommercialUse" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#ShareAlike" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + inkscape:groupmode="layer" + id="layer4" + inkscape:label="plates" + style="opacity:0;display:inline" + transform="translate(-7,-7)" + sodipodi:insensitive="true"> + <rect + inkscape:label="24x24" + y="139" + x="270" + height="24" + width="24" + id="rect4517" + style="fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8246);enable-background:accumulate" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/22x22/ingen.png" + inkscape:export-xdpi="82.5" + inkscape:export-ydpi="82.5" /> + <rect + inkscape:label="22x22" + y="140" + x="271" + height="22" + width="22" + id="rect6749" + style="fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8216);enable-background:accumulate" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/22x22/ingen.png" + inkscape:export-xdpi="82.5" + inkscape:export-ydpi="82.5" + transform="matrix(1.0909077,0,0,1.0909077,-25.635966,-13.727059)" /> + <rect + style="fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8206);enable-background:accumulate" + id="rect6833" + width="16" + height="16" + x="270" + y="247" + inkscape:label="16x16" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_016px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" /> + </g> + <g + inkscape:groupmode="layer" + id="layer5" + inkscape:label="main" + style="display:inline" + transform="translate(-7,-7)"> + <rect + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8166);enable-background:accumulate" + id="rect3494" + width="45.02129" + height="45.02129" + x="270.5" + y="7.5000114" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/48x48/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="5" + ry="5" + transform="matrix(1.0434783,0,0,1.0434783,-12.739131,-1.3043479)" /> + <rect + style="fill:none;stroke:url(#linearGradient3647);stroke-width:0.99999988;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8156);enable-background:accumulate" + id="rect3504" + width="45" + height="45" + x="-52.5" + y="270.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/48x48/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="5" + ry="5" + transform="matrix(0,-1.0434783,1.0434783,0,-12.739131,-1.3043479)" /> + <g + id="g3498" + style="color:#000000;fill:none;stroke:url(#linearGradient8782-9);stroke-width:11.91226196;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8146);enable-background:new" + transform="matrix(0.17345854,0,0,0.17694678,269.84292,6.7497824)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/48x48/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path3500" + d="m 94.869284,131.4843 c 37.317636,0 37.383786,-70.414921 73.570906,-70.414921" + style="color:#000000;fill:none;stroke:url(#linearGradient10806);stroke-width:11.91226196;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + inkscape:connector-curvature="0" /> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path3502" + d="m 94.869284,131.891 c 36.707536,0 37.027296,70.98533 73.570906,70.98533" + style="color:#000000;fill:none;stroke:url(#linearGradient10808);stroke-width:11.91226196;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + inkscape:connector-curvature="0" /> + </g> + <g + id="g3796" + transform="matrix(1.0434783,0,0,1.0434783,-11.945409,-2.2651252)" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient3553-9-7);stroke-width:2.04799986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8136);enable-background:new" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/48x48/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <g + transform="matrix(0.1571435,0,0,0.1570438,270.45094,10.386101)" + id="g3508" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient10788);stroke-width:13.03680897;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new"> + <rect + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient10782);stroke-width:13.03680897;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3510" + width="70" + height="70" + x="25.5" + y="96.499992" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + inkscape:export-xdpi="117.03309" + inkscape:export-ydpi="117.03309" + rx="10" + ry="10" /> + <rect + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient10784);stroke-width:13.03680897;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3512" + width="70" + height="70" + x="25.5" + y="96.499992" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + inkscape:export-xdpi="117.03309" + inkscape:export-ydpi="117.03309" + rx="10" + ry="10" /> + <rect + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient10786);stroke-width:13.03680897;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3514" + width="70" + height="70" + x="25.5" + y="96.499992" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + inkscape:export-xdpi="117.03309" + inkscape:export-ydpi="117.03309" + rx="10" + ry="10" /> + </g> + <g + transform="matrix(0.1571435,0,0,0.157147,294.45103,-1.6241478)" + id="g3524" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient10796);stroke-width:13.03252792;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new"> + <rect + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient10790);stroke-width:13.03252792;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3526" + width="70" + height="70" + x="25.5" + y="96.499992" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + inkscape:export-xdpi="117.03309" + inkscape:export-ydpi="117.03309" + rx="10" + ry="10" /> + <rect + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient10792);stroke-width:13.03252792;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3528" + width="70" + height="70" + x="25.5" + y="96.499992" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + inkscape:export-xdpi="117.03309" + inkscape:export-ydpi="117.03309" + rx="10" + ry="10" /> + <rect + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient10794);stroke-width:13.03252792;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3530" + width="70" + height="70" + x="25.5" + y="96.499992" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + inkscape:export-xdpi="117.03309" + inkscape:export-ydpi="117.03309" + rx="10" + ry="10" /> + </g> + <g + transform="matrix(0.1571435,0,0,0.157147,294.45103,22.369208)" + id="g3768" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient10804);stroke-width:13.03252792;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new"> + <rect + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient10798);stroke-width:13.03252792;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3770" + width="70" + height="70" + x="25.5" + y="96.499992" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + inkscape:export-xdpi="117.03309" + inkscape:export-ydpi="117.03309" + rx="10" + ry="10" /> + <rect + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient10800);stroke-width:13.03252792;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3772" + width="70" + height="70" + x="25.5" + y="96.499992" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + inkscape:export-xdpi="117.03309" + inkscape:export-ydpi="117.03309" + rx="10" + ry="10" /> + <rect + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient10802);stroke-width:13.03252792;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3774" + width="70" + height="70" + x="25.5" + y="96.499992" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + inkscape:export-xdpi="117.03309" + inkscape:export-ydpi="117.03309" + rx="10" + ry="10" /> + </g> + </g> + <rect + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8116);enable-background:accumulate" + id="rect3848" + width="30.014193" + height="30.014193" + x="270.49997" + y="100.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="3" + ry="3" + transform="matrix(1.032258,0,0,1.032258,-9.2096606,-21.7258)" /> + <rect + style="fill:none;stroke:url(#linearGradient4050);stroke-width:0.99999976;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8106);enable-background:accumulate" + id="rect3852" + width="30.000002" + height="30.000002" + x="-130.5" + y="270.49997" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="3" + ry="3" + transform="matrix(0,-1.032258,1.032258,0,-9.2096606,-21.7258)" /> + <g + id="g3856" + style="color:#000000;fill:none;stroke:url(#linearGradient8782-8);stroke-width:13.10617638;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8096);enable-background:new" + transform="matrix(0.1190946,0,0,0.11719659,269.28351,82.047818)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path3858" + d="m 94.869284,131.4843 c 37.317636,0 37.383786,-70.414921 73.570906,-70.414921" + style="color:#000000;fill:none;stroke:url(#linearGradient10778);stroke-width:13.10617638;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + inkscape:connector-curvature="0" /> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path3860" + d="m 94.869284,131.891 c 36.707536,0 37.027296,70.98533 73.570906,70.98533" + style="color:#000000;fill:none;stroke:url(#linearGradient10780);stroke-width:13.10617638;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + inkscape:connector-curvature="0" /> + </g> + <g + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient3557-6-2-6);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-8);enable-background:new" + id="g3872" + transform="matrix(0.10175672,0,0,0.10175691,270.51811,84.133548)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect3874" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10772);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-8);enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect3876" + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10774);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-8);enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect3878" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10776);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-8);enable-background:new" /> + </g> + <g + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient3557-6-2-1);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-7);enable-background:new" + id="g4162" + transform="matrix(0.10175672,0,0,0.10175691,287.1378,75.823691)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4164" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10766);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-7);enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4166" + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10768);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-7);enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4168" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10770);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-7);enable-background:new" /> + </g> + <g + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient3557-6-2-9);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-6);enable-background:new" + id="g4176" + transform="matrix(0.10175672,0,0,0.10175691,287.13738,92.442978)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4178" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10760);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-6);enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4180" + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10762);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-6);enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4182" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient10764);stroke-width:15.2165432;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-6);enable-background:new" /> + </g> + <rect + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8056);enable-background:accumulate" + id="rect4192" + width="21.009962" + height="21.009962" + x="271.5" + y="140.49998" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="120.00015" + inkscape:export-ydpi="120.00015" + rx="3" + ry="3" + transform="matrix(1.0909077,0,0,1.0909077,-25.635966,-13.727059)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:0.99999988;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8046);enable-background:accumulate" + id="rect4196" + width="21.000029" + height="21.000029" + x="-199.50002" + y="271.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="120.00015" + inkscape:export-ydpi="120.00015" + rx="3" + ry="3" + transform="matrix(0,-1.0909077,1.0909077,0,-25.635966,-55.181551)" /> + <path + style="color:#000000;fill:none;stroke:url(#linearGradient5684);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8036);enable-background:new" + d="m 278.90454,150.46569 5.821,-5.68419" + id="path4202" + sodipodi:nodetypes="cc" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="120.00015" + inkscape:export-ydpi="120.00015" + inkscape:connector-curvature="0" + transform="matrix(1.0909077,0,0,1.0909077,-25.370802,-13.196731)" /> + <path + style="color:#000000;fill:none;stroke:url(#linearGradient5682);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8026);enable-background:new" + d="m 278.90454,150.49437 5.60225,5.28692" + id="path4204" + sodipodi:nodetypes="cc" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="120.00015" + inkscape:export-ydpi="120.00015" + inkscape:connector-curvature="0" + transform="matrix(1.0909077,0,0,1.0909077,-25.370802,-13.196731)" /> + <g + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient3557-6-2);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016);enable-background:new" + id="g4206" + transform="matrix(0.06234102,0,0,0.06234102,272.49367,142.78706)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="120.00015" + inkscape:export-ydpi="120.00015"> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4208" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5676);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4210" + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5678);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4212" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient5680);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + </g> + <g + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient5674);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8006);enable-background:new" + id="g4421" + transform="matrix(0.06234102,0,0,0.06234102,283.40254,137.33262)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="120.00015" + inkscape:export-ydpi="120.00015"> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4423" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5668);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4425" + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5670);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4427" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient5672);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + </g> + <g + style="color:#000000;fill:none;stroke:url(#linearGradient5666);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7996);enable-background:new" + id="g4435" + transform="matrix(0.06234102,0,0,0.06234102,283.40254,148.24146)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="120.00015" + inkscape:export-ydpi="120.00015"> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4437" + style="color:#000000;fill:none;stroke:url(#linearGradient5660);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4439" + style="opacity:0.5;color:#000000;fill:none;stroke:url(#linearGradient5662);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4441" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient5664);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + </g> + <rect + style="opacity:0.5;fill:url(#linearGradient3658);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7986);enable-background:accumulate" + id="rect3656" + width="44.000004" + height="44" + x="-52.000004" + y="271" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/48x48/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="4" + ry="4" + transform="matrix(0,-1.021245,1.0227272,0,-6.748686,-0.44433707)" /> + <rect + style="color:#000000;fill:none;stroke:url(#linearGradient3654-2);stroke-width:2.875;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7976);enable-background:accumulate" + id="rect3506" + width="43" + height="43.000004" + x="-51.5" + y="271.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/48x48/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="4" + ry="4" + transform="matrix(0,-1.0461604,1.0464822,0,-13.662087,-1.3640956)" /> + <rect + style="opacity:0.6;fill:url(#linearGradient3688);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7966);enable-background:accumulate" + id="rect3686" + width="29.000002" + height="29" + x="-130" + y="271" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="2.6363637" + ry="2.6363637" + transform="matrix(0,-1.032258,1.0266963,0,-7.7635377,-21.591792)" /> + <rect + style="color:#000000;fill:none;stroke:url(#linearGradient3654-2-1);stroke-width:1.93750012;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7976-6);enable-background:accumulate" + id="rect3854" + width="27.999998" + height="28" + x="-129.5" + y="271.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="2" + ry="2" + transform="matrix(0,-1.0679558,1.0661067,0,-18.880526,-25.830088)" /> + <rect + style="opacity:0.7;fill:url(#linearGradient3819);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7946);enable-background:accumulate" + id="rect3817" + width="20.000002" + height="20" + x="-199" + y="272" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="120.00015" + inkscape:export-ydpi="120.00015" + rx="2.5" + ry="2.5" + transform="matrix(0,-1.0909077,1.0909077,0,-25.645401,-55.130086)" /> + <rect + style="color:#000000;fill:none;stroke:url(#linearGradient3654-3-6-2);stroke-width:1.83333564;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7976-7);enable-background:accumulate" + id="rect4198" + width="19.000015" + height="19" + x="-198.5" + y="272.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/32x32/ingen.png" + inkscape:export-xdpi="120.00015" + inkscape:export-ydpi="120.00015" + rx="2" + ry="2" + transform="matrix(0,-1.151999,1.1519999,0,-42.863959,-66.727812)" /> + <rect + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.99999976;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7926);enable-background:accumulate" + id="rect3752" + width="14.999999" + height="15.000001" + x="-262.5" + y="270.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/16x16/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="2" + ry="2" + transform="matrix(0,-1,1,0,0,0)" /> + <rect + style="color:#000000;fill:none;stroke:url(#linearGradient3654-3-6-5);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7976-2);enable-background:accumulate" + id="rect3754" + width="13" + height="13" + x="-261.5" + y="271.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/16x16/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="1" + ry="1" + transform="matrix(0,-1.1428571,1.1428571,0,-39.714286,-36.428571)" /> + <g + id="g3756" + style="color:#000000;fill:none;stroke:url(#linearGradient8782-5);stroke-width:24.47449875;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7906);enable-background:new" + transform="matrix(0.04304341,0,0,0.03878517,271.99284,249.97528)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/16x16/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path3758" + d="M 94.869284,125.21727 178.70754,34.976303" + style="color:#000000;fill:none;stroke:url(#linearGradient7102);stroke-width:24.47449875;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + inkscape:connector-curvature="0" /> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path3760" + d="m 94.869284,125.62397 86.918466,93.77454" + style="color:#000000;fill:none;stroke:url(#linearGradient7104);stroke-width:24.47449875;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + inkscape:connector-curvature="0" /> + </g> + <rect + style="fill:#00b41e;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7896);enable-background:new" + id="rect3762" + width="2.9998779" + height="2.9998777" + x="273.15637" + y="253.34375" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/16x16/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" /> + <rect + style="fill:#00b41e;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999982;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7886);enable-background:new" + id="rect3764" + width="2.9999928" + height="3" + x="279.15625" + y="250.34375" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/16x16/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" /> + <rect + style="fill:#00b41e;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.99999994;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7876);enable-background:new" + id="rect3766" + width="2.9998782" + height="2.9998779" + x="279.15625" + y="256.34387" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/16x16/ingen.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" /> + </g> + <g + inkscape:groupmode="layer" + id="layer1" + inkscape:label="large" + style="display:inline"> + <rect + inkscape:label="24x24" + y="174.70354" + x="295.23788" + height="24" + width="24" + id="rect4517-0" + style="opacity:0;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8246-6);enable-background:accumulate" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816" + transform="matrix(0.91666667,0,0,0.91666667,-8.131678,19.558628)" /> + <rect + inkscape:label="22x22" + y="175.70354" + x="296.23788" + height="22" + width="22" + id="rect6749-9" + style="opacity:0;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8216-2);enable-background:accumulate" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816" + transform="matrix(0.91666667,0,0,0.91666667,-8.131678,19.558628)" /> + <rect + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8056-6);enable-background:accumulate" + id="rect4192-4" + width="21.009962" + height="21.009962" + x="296.73788" + y="176.20352" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816" + rx="3" + ry="3" + transform="matrix(0.91666667,0,0,0.91666667,-8.131678,19.558628)" /> + <rect + style="fill:none;stroke:#000000;stroke-width:0.99999988;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8046-3);enable-background:accumulate" + id="rect4196-8" + width="21.000029" + height="21.000029" + x="-199.50002" + y="271.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816" + rx="3" + ry="3" + transform="matrix(0,-0.9999987,0.9999987,0,-8.496575,1.7037813)" /> + <path + style="color:#000000;fill:none;stroke:url(#linearGradient5319);stroke-width:0.99999869;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8036-0);enable-background:new" + d="m 270.40761,190.16923 5.821,-5.68418" + id="path4202-3" + sodipodi:nodetypes="cc" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816" + inkscape:connector-curvature="0" /> + <path + style="color:#000000;fill:none;stroke:url(#linearGradient8782-5-1);stroke-width:0.99999869;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8026-0);enable-background:new" + d="m 270.40761,190.19791 5.60225,5.28691" + id="path4204-0" + sodipodi:nodetypes="cc" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816" + inkscape:connector-curvature="0" /> + <g + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient5327);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8016-2);enable-background:new" + id="g4206-8" + transform="matrix(0.05714593,0,0,0.05714593,264.54586,182.68887)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816"> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4208-1" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5321);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4210-9" + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5323);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4212-7" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient5325);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + </g> + <g + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient5335);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8006-6);enable-background:new" + id="g4421-1" + transform="matrix(0.05714593,0,0,0.05714593,274.54566,177.68897)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816"> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4423-4" + style="color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5329);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4425-0" + style="opacity:0.5;color:#000000;fill:#003f0a;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5331);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4427-1" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient5333);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + </g> + <g + style="color:#000000;fill:none;stroke:url(#linearGradient3557-6-2-2);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7996-4);enable-background:new" + id="g4435-7" + transform="matrix(0.05714593,0,0,0.05714593,274.54566,187.68873)" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816"> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4437-9" + style="color:#000000;fill:none;stroke:url(#linearGradient5337);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4439-7" + style="opacity:0.5;color:#000000;fill:none;stroke:url(#linearGradient5339);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + <rect + ry="10" + rx="10" + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + y="96.499992" + x="25.5" + height="70" + width="70" + id="rect4441-6" + style="color:#000000;fill:#003f0a;fill-opacity:1;stroke:url(#linearGradient5341);stroke-width:17.49903488;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" /> + </g> + <rect + style="opacity:0.7;fill:url(#linearGradient3819-7);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7946-1);enable-background:accumulate" + id="rect3817-0" + width="20.000002" + height="20" + x="-199" + y="272" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816" + rx="2.5" + ry="2.5" + transform="matrix(0,-0.9999987,0.9999987,0,-8.505225,1.7509609)" /> + <rect + style="color:#000000;fill:none;stroke:url(#linearGradient3654-3-6-2-6);stroke-width:2.00000262;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter7976-7-3);enable-background:accumulate" + id="rect4198-8" + width="19.000015" + height="19" + x="-198.5" + y="272.5" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + inkscape:export-xdpi="98.181816" + inkscape:export-ydpi="98.181816" + rx="2" + ry="2" + transform="matrix(0,-1.0476182,1.0476189,0,-21.925476,-7.2962954)" /> + <rect + style="fill:url(#linearGradient3950);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect2573" + width="254.79922" + height="254.79922" + x="-9.3112211" + y="-0.81120729" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="11.943713" + ry="11.943713" /> + <g + transform="matrix(1.024,0,0,1.024,-17.168001,-8.6679843)" + id="g3450" + style="stroke:url(#linearGradient3460-9);stroke-width:3.90625;stroke-miterlimit:4;stroke-dasharray:none;display:inline;enable-background:new" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077"> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path2785" + d="m 94.869284,131.4843 c 37.317636,0 37.383786,-70.414921 73.570906,-70.414921" + style="fill:none;stroke:url(#linearGradient3051-7);stroke-width:3.90625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + inkscape:connector-curvature="0" /> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path2787" + d="m 94.869284,131.891 c 36.707536,0 37.027296,70.98533 73.570906,70.98533" + style="fill:none;stroke:url(#linearGradient3053-4);stroke-width:3.90625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + inkscape:connector-curvature="0" /> + </g> + <rect + style="fill:url(#linearGradient4372-9);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3389" + width="71.68" + height="71.68" + x="8.9439974" + y="90.14801" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="10.24" + ry="10.24" /> + <rect + style="opacity:0.5;fill:url(#linearGradient4369-1);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3402" + width="71.68" + height="71.68" + x="8.9439974" + y="90.14801" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="10.24" + ry="10.24" /> + <rect + style="fill:none;stroke:url(#linearGradient4366-2);stroke-width:2.04799986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3404" + width="70.655998" + height="70.655998" + x="9.4559984" + y="90.660004" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="10.093715" + ry="10.093715" /> + <rect + style="fill:url(#linearGradient4361-5);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3424" + width="71.68" + height="71.68" + x="154.35201" + y="162.85201" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="10.24" + ry="10.24" /> + <rect + style="opacity:0.5;fill:url(#linearGradient4358-7);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3426" + width="71.68" + height="71.68" + x="154.35201" + y="162.85201" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="10.24" + ry="10.24" /> + <rect + style="fill:url(#linearGradient4381-4);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3438" + width="71.68" + height="71.68" + x="154.35201" + y="17.444004" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="10.24" + ry="10.24" /> + <rect + style="opacity:0.5;fill:url(#linearGradient4378-8);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3440" + width="71.68" + height="71.68" + x="154.35201" + y="17.444004" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="10.24" + ry="10.24" /> + <rect + style="fill:none;stroke:url(#linearGradient3553-1);stroke-width:2.04799986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3551" + width="70.655998" + height="70.655998" + x="154.864" + y="17.956005" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="10.093715" + ry="10.093715" /> + <rect + style="fill:none;stroke:url(#linearGradient3557-7);stroke-width:2.04799986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3555" + width="70.655998" + height="70.655998" + x="154.864" + y="163.364" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="10.093715" + ry="10.093715" /> + <path + style="opacity:0.3;fill:url(#linearGradient3056-7);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + d="m -8.2379452,240.9007 c 0,-76.42105 0,-152.842095 0,-229.263148 C -8.3994802,1.2188777 2.8139628,-0.40249629 10.841615,0.42912971 c 73.968673,0 147.937335,0 221.906025,0 10.44094,-0.16119 12.06579,11.02833429 11.23238,19.03886629 0,73.810895 0,147.621814 0,221.432704 0.16155,10.41868 -11.05191,12.04005 -19.07957,11.20842 -73.96867,0 -147.937346,0 -221.9060192,0 -6.037692,0.14548 -11.377692,-5.18156 -11.232376,-11.20842 z" + id="rect3423" + inkscape:connector-curvature="0" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" /> + <rect + style="fill:none;stroke:url(#linearGradient3654);stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3649" + width="252.00728" + height="252.00728" + x="-252.50365" + y="-8.0036421" + inkscape:label="scalable" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/512x512/ingen.png" + inkscape:export-xdpi="177.23077" + inkscape:export-ydpi="177.23077" + rx="11.222997" + ry="11.222998" + transform="matrix(0,-1,1,0,0,0)" /> + </g> +</svg> diff --git a/icons/scalable/ingen.svg b/icons/scalable/ingen.svg new file mode 100644 index 00000000..980cc418 --- /dev/null +++ b/icons/scalable/ingen.svg @@ -0,0 +1,611 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + inkscape:export-ydpi="8.3996801" + inkscape:export-xdpi="8.3996801" + inkscape:export-filename="/home/drobilla/src/lad/trunk/ingen/icons/24x24/ingen.png" + width="311" + height="256" + id="svg11300" + sodipodi:version="0.32" + inkscape:version="0.48.5 r10040" + sodipodi:docname="ingen.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + version="1.0" + style="display:inline;enable-background:new"> + <defs + id="defs3"> + <linearGradient + id="linearGradient3635"> + <stop + style="stop-color:#c0c0c0;stop-opacity:1;" + offset="0" + id="stop3637" /> + <stop + id="stop3643" + offset="0.47374481" + style="stop-color:#00bd56;stop-opacity:0;" /> + <stop + style="stop-color:#71ff95;stop-opacity:0.33333334;" + offset="0.70321494" + id="stop3645" /> + <stop + style="stop-color:#008441;stop-opacity:0.35294119;" + offset="1" + id="stop3639" /> + </linearGradient> + <linearGradient + id="linearGradient3621"> + <stop + style="stop-color:#000b24;stop-opacity:1;" + offset="0" + id="stop3627" /> + <stop + id="stop3629" + offset="1" + style="stop-color:#000000;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3611"> + <stop + id="stop3613" + offset="0" + style="stop-color:#22bc55;stop-opacity:1;" /> + <stop + style="stop-color:#1a9844;stop-opacity:1;" + offset="0.16842106" + id="stop3615" /> + <stop + id="stop3617" + offset="1" + style="stop-color:#196834;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3404"> + <stop + id="stop3406" + offset="0" + style="stop-color:#236eda;stop-opacity:1;" /> + <stop + style="stop-color:#2dabf5;stop-opacity:1;" + offset="0.5" + id="stop3408" /> + <stop + id="stop3411" + offset="1" + style="stop-color:#347feb;stop-opacity:1;" /> + </linearGradient> + <linearGradient + id="linearGradient3454"> + <stop + style="stop-color:#28ac3b;stop-opacity:1;" + offset="0" + id="stop3456" /> + <stop + id="stop3462" + offset="0.5" + style="stop-color:#0fa123;stop-opacity:1;" /> + <stop + style="stop-color:#63da74;stop-opacity:1;" + offset="1" + id="stop3458" /> + </linearGradient> + <linearGradient + id="linearGradient3408"> + <stop + style="stop-color:#76e99d;stop-opacity:1;" + offset="0" + id="stop3410" /> + <stop + style="stop-color:#00561c;stop-opacity:1;" + offset="1" + id="stop3412" /> + </linearGradient> + <linearGradient + id="linearGradient3392"> + <stop + style="stop-color:#5acc6b;stop-opacity:1;" + offset="0" + id="stop3394" /> + <stop + id="stop3400" + offset="0.5" + style="stop-color:#229e33;stop-opacity:1;" /> + <stop + style="stop-color:#196f26;stop-opacity:1;" + offset="1" + id="stop3396" /> + </linearGradient> + <linearGradient + id="linearGradient3372"> + <stop + style="stop-color:#222b31;stop-opacity:1;" + offset="0" + id="stop3374" /> + <stop + style="stop-color:#384750;stop-opacity:0.68627453;" + offset="1" + id="stop3376" /> + </linearGradient> + <linearGradient + id="linearGradient3415"> + <stop + style="stop-color:#092513;stop-opacity:1;" + offset="0" + id="stop3417" /> + <stop + style="stop-color:#145c2d;stop-opacity:1;" + offset="1" + id="stop3419" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3621" + id="linearGradient3405" + x1="105.5" + y1="7.5" + x2="105.5" + y2="256.62848" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0232901,0,0,1.0232901,-9.9858987,-9.9858987)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3415" + id="linearGradient3421" + x1="-257" + y1="132" + x2="-7" + y2="132" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.024,0,0,1.024,10.168,-10.168)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3372" + id="linearGradient3378" + x1="-256.5" + y1="132" + x2="-7.5" + y2="132" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-6.5,6.5)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient3460" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3404" + id="linearGradient3832" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient4358" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,135.24,62.535996)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient4361" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,135.24,62.535996)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408" + id="linearGradient4366" + gradientUnits="userSpaceOnUse" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" + gradientTransform="matrix(1.0093714,0,0,1.0093714,-9.282967,-8.2443433)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient4369" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,-10.168,-10.168)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient4372" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,-10.168,-10.168)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient4378" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,135.24,-82.872002)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3392" + id="linearGradient4381" + gradientUnits="userSpaceOnUse" + x1="45.5" + y1="95.999992" + x2="45.5" + y2="167.00703" + gradientTransform="matrix(1.024,0,0,1.024,135.24,-82.872002)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408" + id="linearGradient3553" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,136.12503,-80.948345)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3408" + id="linearGradient3557" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0093714,0,0,1.0093714,136.12503,64.45965)" + x1="44.5" + y1="95.999992" + x2="54" + y2="167" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635" + id="linearGradient3641" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.024,0,0,1.024,3.0000057,-2.9999919)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3611" + id="linearGradient3654" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.0202725,0,0,1.0202725,9.6759793,-9.6759565)" + x1="-8.0000153" + y1="109.99998" + x2="-255.99998" + y2="160.49998" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient3051" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3454" + id="linearGradient3053" + gradientUnits="userSpaceOnUse" + x1="93.869286" + y1="131.97285" + x2="169.44019" + y2="131.97285" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient3635" + id="linearGradient3056" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1.0189475,1.0211255,0,-2.7696321,-2.599321)" + x1="-1" + y1="52.999992" + x2="-249" + y2="124.99999" /> + </defs> + <sodipodi:namedview + stroke="#ef2929" + fill="#f57900" + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="0.25490196" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1" + inkscape:cx="194.07459" + inkscape:cy="35.977515" + inkscape:current-layer="layer1" + showgrid="false" + inkscape:grid-bbox="true" + inkscape:document-units="px" + inkscape:showpageshadow="false" + inkscape:window-width="2556" + inkscape:window-height="1574" + inkscape:window-x="0" + inkscape:window-y="22" + width="400px" + height="300px" + inkscape:snap-nodes="false" + inkscape:snap-bbox="true" + objecttolerance="7" + gridtolerance="12" + guidetolerance="13" + showborder="false" + showguides="true" + inkscape:guide-bbox="true" + inkscape:window-maximized="0"> + <inkscape:grid + type="xygrid" + id="grid5883" + spacingx="1px" + spacingy="1px" + enabled="true" + visible="true" + empspacing="5" /> + </sodipodi:namedview> + <metadata + id="metadata4"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:creator> + <cc:Agent> + <dc:title>Breathe Icon Team</dc:title> + </cc:Agent> + </dc:creator> + <dc:source /> + <cc:license + rdf:resource="http://creativecommons.org/licenses/by-nc-sa/3.0/" /> + <dc:title>Breathe Icon Set template</dc:title> + <dc:subject> + <rdf:Bag /> + </dc:subject> + <dc:date /> + <dc:rights> + <cc:Agent> + <dc:title /> + </cc:Agent> + </dc:rights> + <dc:publisher> + <cc:Agent> + <dc:title /> + </cc:Agent> + </dc:publisher> + <dc:identifier /> + <dc:relation /> + <dc:language /> + <dc:coverage /> + <dc:description /> + <dc:contributor> + <cc:Agent> + <dc:title>Jakub Steiner, Cory Kontros</dc:title> + </cc:Agent> + </dc:contributor> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/by-nc-sa/3.0/"> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Notice" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#Attribution" /> + <cc:prohibits + rdf:resource="http://creativecommons.org/ns#CommercialUse" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> + <cc:requires + rdf:resource="http://creativecommons.org/ns#ShareAlike" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + inkscape:groupmode="layer" + id="layer1" + inkscape:label="large" + style="display:inline"> + <rect + style="fill:url(#linearGradient3405);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect2573" + width="254.79922" + height="254.79922" + x="-2.3112233" + y="-2.3112233" + inkscape:label="scalable" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="11.943713" + ry="11.943713" /> + <g + transform="matrix(1.024,0,0,1.024,-10.168,-10.168)" + id="g3450" + style="stroke:url(#linearGradient3460);stroke-width:3.90625;stroke-miterlimit:4;stroke-dasharray:none;display:inline;enable-background:new" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path2785" + d="m 94.869284,131.4843 c 37.317636,0 37.383786,-70.414921 73.570906,-70.414921" + style="fill:none;stroke:url(#linearGradient3051);stroke-width:3.90625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + inkscape:connector-curvature="0" /> + <path + inkscape:export-ydpi="117.03309" + inkscape:export-xdpi="117.03309" + inkscape:export-filename="/mnt/first/logos/ingen/ingen_10.png" + sodipodi:nodetypes="cc" + id="path2787" + d="m 94.869284,131.891 c 36.707536,0 37.027296,70.98533 73.570906,70.98533" + style="fill:none;stroke:url(#linearGradient3053);stroke-width:3.90625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" + inkscape:connector-curvature="0" /> + </g> + <rect + style="fill:url(#linearGradient4372);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3389" + width="71.68" + height="71.68" + x="15.944" + y="88.647995" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="10.24" + ry="10.24" /> + <rect + style="opacity:0.5;fill:url(#linearGradient4369);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3402" + width="71.68" + height="71.68" + x="15.944" + y="88.647995" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="10.24" + ry="10.24" /> + <rect + style="fill:none;stroke:url(#linearGradient4366);stroke-width:2.04799986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3404" + width="70.655998" + height="70.655998" + x="16.455999" + y="89.159988" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="10.093715" + ry="10.093715" /> + <rect + style="fill:url(#linearGradient4361);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3424" + width="71.68" + height="71.68" + x="161.35201" + y="161.35199" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="10.24" + ry="10.24" /> + <rect + style="opacity:0.5;fill:url(#linearGradient4358);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3426" + width="71.68" + height="71.68" + x="161.35201" + y="161.35199" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="10.24" + ry="10.24" /> + <rect + style="fill:url(#linearGradient4381);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3438" + width="71.68" + height="71.68" + x="161.35201" + y="15.943991" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="10.24" + ry="10.24" /> + <rect + style="opacity:0.5;fill:url(#linearGradient4378);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3440" + width="71.68" + height="71.68" + x="161.35201" + y="15.943991" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="10.24" + ry="10.24" /> + <rect + style="fill:none;stroke:url(#linearGradient3553);stroke-width:2.04799986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3551" + width="70.655998" + height="70.655998" + x="161.864" + y="16.45599" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="10.093715" + ry="10.093715" /> + <rect + style="fill:none;stroke:url(#linearGradient3557);stroke-width:2.04799986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:new" + id="rect3555" + width="70.655998" + height="70.655998" + x="161.864" + y="161.86398" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="10.093715" + ry="10.093715" /> + <path + style="opacity:0.3;fill:url(#linearGradient3056);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + d="m -1.2379439,239.40068 c 0,-76.42104 0,-152.842089 0,-229.263142 -0.161535,-10.41867371 11.0519076,-12.0400476 19.0795599,-11.2084216 73.968673,0 147.937334,0 221.906024,0 10.44094,-0.1611906 12.06579,11.0283344 11.23238,19.0388656 0,73.810895 0,147.621818 0,221.432698 0.16155,10.41868 -11.05191,12.04005 -19.07957,11.20842 -73.96867,0 -147.937344,0 -221.9060177,0 -6.0376914,0.14548 -11.3776921,-5.18156 -11.2323762,-11.20842 z" + id="rect3423" + inkscape:connector-curvature="0" /> + <rect + style="fill:none;stroke:url(#linearGradient3654);stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" + id="rect3649" + width="252.00728" + height="252.00728" + x="-251.00365" + y="-1.0036429" + inkscape:label="scalable" + inkscape:export-filename="/media/sdb8/software_artwork/ingen/ingen_icon_256px.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + rx="11.222997" + ry="11.222998" + transform="matrix(0,-1,1,0,0,0)" /> + </g> +</svg> diff --git a/ingen.ttl b/ingen.ttl new file mode 100644 index 00000000..a75c73b5 --- /dev/null +++ b/ingen.ttl @@ -0,0 +1,35 @@ +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix foaf: <http://xmlns.com/foaf/0.1/> . + +<http://drobilla.net/drobilla#me> + a foaf:Person ; + foaf:name "David Robillard" ; + foaf:mbox <mailto:d@drobilla.net> ; + rdfs:seeAlso <http://drobilla.net/drobilla> . + +<http://drobilla.net/software/ingen> + a doap:Project ; + doap:name "Ingen" ; + doap:shortdesc "A modular audio processing environment." ; + doap:homepage <http://drobilla.net/software/ingen/> ; + doap:bug-database <http://dev.drobilla.net/> ; + doap:license <http://usefulinc.com/doap/licenses/gpl> ; + doap:developer <http://drobilla.net/drobilla#me> ; + doap:programming-language "C" , + "C++" ; + doap:repository [ + a doap:SVNRepository ; + doap:browse <http://dev.drobilla.net/browser/trunk/ingen> ; + doap:location <http://svn.drobilla.net/lad/trunk/ingen> + ] ; + doap:description """ +A modular audio processing environment, where most functionality is provided +by generic plugins. Patching of audio, MIDI, and control data is supported. +The engine and UI are completely separated in a network transparent way, making +it possible to run one or more GUIs on separate machines from the engine. + +Ingen is closely based on LV2 plugin technology; Ingen graphs are RDF documents +in Turtle in a very similar format to the RDF definition of an LV2 plugin. +""" . diff --git a/ingen/Arc.hpp b/ingen/Arc.hpp new file mode 100644 index 00000000..043ffc0b --- /dev/null +++ b/ingen/Arc.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_ARC_HPP +#define INGEN_ARC_HPP + +#include "ingen/ingen.h" +#include "raul/Deletable.hpp" + +namespace Raul { class Path; } + +namespace Ingen { + +/** A connection between two ports. + * + * @ingroup Ingen + */ +class INGEN_API Arc : public Raul::Deletable +{ +public: + virtual const Raul::Path& tail_path() const = 0; + virtual const Raul::Path& head_path() const = 0; +}; + +} // namespace Ingen + +#endif // INGEN_ARC_HPP diff --git a/ingen/Atom.hpp b/ingen/Atom.hpp new file mode 100644 index 00000000..5e90903a --- /dev/null +++ b/ingen/Atom.hpp @@ -0,0 +1,178 @@ +/* + 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_ATOM_HPP +#define INGEN_ATOM_HPP + +#include <algorithm> +#include <cassert> +#include <cstdint> +#include <cstdlib> +#include <cstring> + +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" + +namespace Ingen { + +/** + A generic typed data container. + + An Atom holds a value with some type and size, both specified by a uint32_t. + Values with size less than sizeof(void*) are stored inline: no dynamic + allocation occurs so Atoms may be created in hard real-time threads. + Otherwise, if the size is larger than sizeof(void*), the value will be + dynamically allocated in a separate chunk of memory. + + In either case, the data is stored in a binary compatible format to LV2_Atom + (i.e., if the value is dynamically allocated, the header is repeated there). +*/ +class INGEN_API Atom { +public: + Atom() noexcept { _atom.size = 0; _atom.type = 0; _body.ptr = nullptr; } + ~Atom() { dealloc(); } + + /** Construct a raw atom. + * + * Typically this is not used directly, use Forge methods to make atoms. + */ + Atom(uint32_t size, LV2_URID type, const void* body) { + _atom.size = size; + _atom.type = type; + _body.ptr = nullptr; + if (is_reference()) { + _body.ptr = (LV2_Atom*)malloc(sizeof(LV2_Atom) + size); + memcpy(_body.ptr, &_atom, sizeof(LV2_Atom)); + } + if (body) { + memcpy(get_body(), body, size); + } + } + + Atom(const Atom& copy) + : _atom(copy._atom) + { + if (is_reference()) { + _body.ptr = (LV2_Atom*)malloc(sizeof(LV2_Atom) + _atom.size); + memcpy(_body.ptr, copy._body.ptr, sizeof(LV2_Atom) + _atom.size); + } else { + _body.val = copy._body.val; + } + } + + Atom& operator=(const Atom& other) { + if (&other == this) { + return *this; + } + dealloc(); + _atom = other._atom; + if (is_reference()) { + _body.ptr = (LV2_Atom*)malloc(sizeof(LV2_Atom) + _atom.size); + memcpy(_body.ptr, other._body.ptr, sizeof(LV2_Atom) + _atom.size); + } else { + _body.val = other._body.val; + } + return *this; + } + + inline bool operator==(const Atom& other) const { + if (_atom.type != other._atom.type || + _atom.size != other._atom.size) { + return false; + } + return is_reference() + ? !memcmp(_body.ptr, other._body.ptr, sizeof(LV2_Atom) + _atom.size) + : _body.val == other._body.val; + } + + inline bool operator!=(const Atom& other) const { + return !operator==(other); + } + + inline bool operator<(const Atom& other) const { + if (_atom.type == other._atom.type) { + const uint32_t min_size = std::min(_atom.size, other._atom.size); + const int cmp = is_reference() + ? memcmp(_body.ptr, other._body.ptr, min_size) + : memcmp(&_body.val, &other._body.val, min_size); + return cmp < 0 || (cmp == 0 && _atom.size < other._atom.size); + } + return type() < other.type(); + } + + /** Like assignment, but only works for value atoms (not references). + * Always real-time safe. + * @return true iff set succeeded. + */ + inline bool set_rt(const Atom& other) { + if (is_reference()) { + return false; + } else { + _atom = other._atom; + _body.val = other._body.val; + return true; + } + } + + inline uint32_t size() const { return _atom.size; } + inline LV2_URID type() const { return _atom.type; } + inline bool is_valid() const { return _atom.type; } + + inline const void* get_body() const { + return is_reference() ? (void*)(_body.ptr + 1) : &_body.val; + } + + inline void* get_body() { + return is_reference() ? (void*)(_body.ptr + 1) : &_body.val; + } + + template <typename T> const T& get() const { + assert(size() == sizeof(T)); + return *static_cast<const T*>(get_body()); + } + + template <typename T> const T* ptr() const { + return static_cast<const T*>(get_body()); + } + + const LV2_Atom* atom() const { + return is_reference() ? _body.ptr : &_atom; + } + +private: + /** Free dynamically allocated value, if applicable. */ + inline void dealloc() { + if (is_reference()) { + free(_body.ptr); + } + } + + /** Return true iff this value is dynamically allocated. */ + inline bool is_reference() const { + return _atom.size > sizeof(_body.val); + } + + LV2_Atom _atom; + union { + intptr_t val; + LV2_Atom* ptr; + } _body; +}; + +} // namespace Ingen + +#endif // INGEN_ATOM_HPP diff --git a/ingen/AtomForgeSink.hpp b/ingen/AtomForgeSink.hpp new file mode 100644 index 00000000..8c070701 --- /dev/null +++ b/ingen/AtomForgeSink.hpp @@ -0,0 +1,102 @@ +/* + 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_ATOMFORGESINK_HPP +#define INGEN_ATOMFORGESINK_HPP + +#include <cassert> +#include <cstdint> +#include <cstdlib> +#include <cstring> + +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +namespace Ingen { + +/** A resizing sink for LV2_Atom_Forge. */ +class AtomForgeSink +{ +public: + explicit AtomForgeSink(LV2_Atom_Forge* forge = nullptr) + : _capacity(8 * sizeof(LV2_Atom)) + , _size(0) + , _buf((LV2_Atom*)calloc(8, sizeof(LV2_Atom))) + { + if (forge) { + set_forge_sink(forge); + } + } + + ~AtomForgeSink() { free(_buf); } + + void set_forge_sink(LV2_Atom_Forge* forge) { + lv2_atom_forge_set_sink(forge, c_append, c_deref, this); + } + + /** Append some data and return a reference to its start. */ + intptr_t append(const void* buf, uint32_t len) { + // Record offset of the start of this write (+1 to avoid NULL) + const intptr_t ref = _size + 1; + + // Update size and reallocate if necessary + if (lv2_atom_pad_size(_size + len) > _capacity) { + _capacity = lv2_atom_pad_size(_size + len); + _buf = (LV2_Atom*)realloc(_buf, _capacity); + } + + // Append new data + memcpy((uint8_t*)_buf + _size, buf, len); + _size += len; + return ref; + } + + /** Dereference a reference previously returned by append. */ + LV2_Atom* deref(intptr_t ref) { + /* Make some assumptions and do unnecessary math to appease + -Wcast-align. This is questionable at best, though the forge should + only dereference references to aligned atoms. */ + assert((ref - 1) % sizeof(LV2_Atom) == 0); + return (LV2_Atom*)(_buf + (ref - 1) / sizeof(LV2_Atom)); + + // Alternatively: + // return (LV2_Atom*)((uint8_t*)_buf + ref - 1); + } + + const LV2_Atom* atom() const { return _buf; } + + void clear() { _buf->type = 0; _buf->size = 0; _size = 0; } + + static LV2_Atom_Forge_Ref + c_append(void* handle, const void* buf, uint32_t len) { + return ((AtomForgeSink*)handle)->append(buf, len); + } + + static LV2_Atom* + c_deref(void* handle, LV2_Atom_Forge_Ref ref) { + return ((AtomForgeSink*)handle)->deref(ref); + } + +private: + size_t _capacity; + size_t _size; + LV2_Atom* _buf; +}; + +} // namespace Ingen + +#endif // INGEN_ATOMFORGESINK_HPP diff --git a/ingen/AtomReader.hpp b/ingen/AtomReader.hpp new file mode 100644 index 00000000..7ea4654a --- /dev/null +++ b/ingen/AtomReader.hpp @@ -0,0 +1,76 @@ +/* + 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_ATOMREADER_HPP +#define INGEN_ATOMREADER_HPP + +#include <cstdint> + +#include <boost/optional/optional.hpp> + +#include "ingen/AtomSink.hpp" +#include "ingen/Resource.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" + +namespace Raul { +class Path; +} + +namespace Ingen { + +class URI; +class Atom; +class Interface; +class Log; +class Properties; +class URIMap; +class URIs; + +/** An AtomSink that calls methods on an Interface. + * @ingroup IngenShared + */ +class INGEN_API AtomReader : public AtomSink +{ +public: + AtomReader(URIMap& map, + URIs& uris, + Log& log, + Interface& iface); + + static bool is_message(const URIs& uris, const LV2_Atom* msg); + + bool write(const LV2_Atom* msg, int32_t default_id=0) override; + +private: + void get_atom(const LV2_Atom* in, Atom& out); + + boost::optional<URI> atom_to_uri(const LV2_Atom* atom); + boost::optional<Raul::Path> atom_to_path(const LV2_Atom* atom); + Resource::Graph atom_to_context(const LV2_Atom* atom); + + void get_props(const LV2_Atom_Object* obj, + Ingen::Properties& props); + + URIMap& _map; + URIs& _uris; + Log& _log; + Interface& _iface; +}; + +} // namespace Ingen + +#endif // INGEN_ATOMREADER_HPP diff --git a/ingen/AtomSink.hpp b/ingen/AtomSink.hpp new file mode 100644 index 00000000..0cfa027a --- /dev/null +++ b/ingen/AtomSink.hpp @@ -0,0 +1,47 @@ +/* + 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_ATOMSINK_HPP +#define INGEN_ATOMSINK_HPP + +#include <cstdint> + +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" + +namespace Ingen { + +/** A sink for LV2 Atoms. + * @ingroup IngenShared + */ +class INGEN_API AtomSink { +public: + virtual ~AtomSink() = default; + + /** Write an Atom to the sink. + * + * @param msg The atom to write. + * @param default_id The default response ID to use if no + * patch:sequenceNumber property is present on the message. + * + * @return True on success. + */ + virtual bool write(const LV2_Atom* msg, int32_t default_id=0) = 0; +}; + +} // namespace Ingen + +#endif // INGEN_ATOMSINK_HPP diff --git a/ingen/AtomWriter.hpp b/ingen/AtomWriter.hpp new file mode 100644 index 00000000..5e765c75 --- /dev/null +++ b/ingen/AtomWriter.hpp @@ -0,0 +1,86 @@ +/* + 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_ATOMWRITER_HPP +#define INGEN_ATOMWRITER_HPP + +#include <cstdint> + +#include "ingen/AtomForgeSink.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Message.hpp" +#include "ingen/Properties.hpp" +#include "ingen/Resource.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" + +namespace Raul { class Path; } + +namespace Ingen { + +class AtomSink; +class URIMap; +class URIs; + +/** An Interface that writes LV2 atoms to an AtomSink. */ +class INGEN_API AtomWriter : public Interface +{ +public: + using result_type = void; ///< For boost::apply_visitor + + AtomWriter(URIMap& map, URIs& uris, AtomSink& sink); + + URI uri() const override { return URI("ingen:/clients/atom_writer"); } + + void message(const Message& message) override; + + 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&); + +private: + void forge_uri(const URI& uri); + void forge_properties(const Properties& properties); + void forge_arc(const Raul::Path& tail, const Raul::Path& head); + void forge_request(LV2_Atom_Forge_Frame* frame, LV2_URID type, int32_t id); + void forge_context(Resource::Graph ctx); + + void finish_msg(); + + URIMap& _map; + URIs& _uris; + AtomSink& _sink; + AtomForgeSink _out; + LV2_Atom_Forge _forge; +}; + +} // namespace Ingen + +#endif // INGEN_ATOMWRITER_HPP diff --git a/ingen/ClashAvoider.hpp b/ingen/ClashAvoider.hpp new file mode 100644 index 00000000..069c8fef --- /dev/null +++ b/ingen/ClashAvoider.hpp @@ -0,0 +1,55 @@ +/* + 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_CLASHAVOIDER_HPP +#define INGEN_CLASHAVOIDER_HPP + +#include <map> + +#include "ingen/ingen.h" +#include "raul/Path.hpp" + +namespace Ingen { + +class Store; +class URI; + +/** Maps paths so they do not clash with an existing object in a store. + * + * @ingroup ingen + */ +class INGEN_API ClashAvoider +{ +public: + explicit ClashAvoider(const Store& store); + + const URI map_uri(const URI& in); + const Raul::Path map_path(const Raul::Path& in); + + bool exists(const Raul::Path& path) const; + +private: + typedef std::map<Raul::Path, unsigned> Offsets; + typedef std::map<Raul::Path, Raul::Path> SymbolMap; + + const Store& _store; + Offsets _offsets; + SymbolMap _symbol_map; +}; + +} // namespace Ingen + +#endif // INGEN_CLASHAVOIDER_HPP diff --git a/ingen/Clock.hpp b/ingen/Clock.hpp new file mode 100644 index 00000000..69d5eb17 --- /dev/null +++ b/ingen/Clock.hpp @@ -0,0 +1,63 @@ +/* + This file is part of Ingen. + Copyright 2016-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_CLOCK_HPP +#define INGEN_ENGINE_CLOCK_HPP + +#include <cstdint> + +#ifdef __MACH__ +# include <mach/mach.h> +# include <mach/mach_time.h> +#else +# include <time.h> +# include <sys/time.h> +#endif + +namespace Ingen { + +class Clock { +public: +#ifdef __MACH__ + + Clock() { mach_timebase_info(&_timebase); } + + inline uint64_t now_microseconds() const { + const uint64_t now = mach_absolute_time(); + return now * _timebase.numer / _timebase.denom / 1e3; + } + +private: + mach_timebase_info_data_t _timebase; + +#else + + inline uint64_t now_microseconds() const { + struct timespec time; +# if defined(CLOCK_MONOTONIC_RAW) + clock_gettime(CLOCK_MONOTONIC_RAW, &time); +# else + clock_gettime(CLOCK_MONOTONIC, &time); +# endif + return (uint64_t)time.tv_sec * 1e6 + (uint64_t)time.tv_nsec / 1e3; + } + +#endif +}; + +} // namespace Ingen + +#endif // INGEN_ENGINE_CLOCK_HPP diff --git a/ingen/ColorContext.hpp b/ingen/ColorContext.hpp new file mode 100644 index 00000000..81e0e062 --- /dev/null +++ b/ingen/ColorContext.hpp @@ -0,0 +1,37 @@ +/* + 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_COLORCONTEXT_HPP +#define INGEN_COLORCONTEXT_HPP + +#include <cstdio> + +namespace Ingen { + +class ColorContext { +public: + enum class Color { RED = 31, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE }; + + ColorContext(FILE* stream, Color color); + ~ColorContext(); + +private: + FILE* _stream; +}; + +} // namespace Ingen + +#endif // INGEN_COLORCONTEXT_HPP diff --git a/ingen/Configuration.hpp b/ingen/Configuration.hpp new file mode 100644 index 00000000..6759b4b0 --- /dev/null +++ b/ingen/Configuration.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_CONFIGURATION_HPP +#define INGEN_CONFIGURATION_HPP + +#include <cstdlib> +#include <list> +#include <map> +#include <ostream> +#include <string> + +#include "ingen/Atom.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "raul/Exception.hpp" + +namespace Ingen { + +class FilePath; +class Forge; +class URIMap; + +/** Ingen configuration (command line options and/or configuration file). + * @ingroup IngenShared + */ +class INGEN_API Configuration { +public: + explicit Configuration(Forge& forge); + + /** The scope of a configuration option. + * + * This controls when and where an option will be saved or restored. + */ + enum Scope { + GLOBAL = 1, ///< Applies to any Ingen instance + SESSION = 1<<1, ///< Applies to this Ingen instance only + GUI = 1<<2 ///< Persistent GUI settings saved at exit + }; + + /** Add a configuration option. + * + * @param key URI local name, in camelCase + * @param name Long option name (without leading "--") + * @param letter Short option name (without leading "-") + * @param desc Description + * @param scope Scope of option + * @param type Type + * @param value Default value + */ + 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); + + void print_usage(const std::string& program, std::ostream& os); + + struct OptionError : public Raul::Exception { + explicit OptionError(const std::string& m) : Exception(m) {} + }; + + struct FileError : public Raul::Exception { + explicit FileError(const std::string& m) : Exception(m) {} + }; + + /** Parse a command line. + * + * @throw OptionError + */ + void parse(int argc, char **argv); + + /** Load a specific file. */ + bool load(const FilePath& path); + + /** Save configuration to a file. + * + * @param uri_map URI map. + * + * @param app Application name. + * + * @param filename If absolute, the configuration will be saved to this + * path. Otherwise the configuration will be saved to the user + * configuration directory (e.g. ~/.config/ingen/filename). + * + * @param scopes Bitwise OR of Scope values. Only options which match the + * given scopes will be saved. + * + * @return The absolute path of the saved configuration file. + */ + FilePath save(URIMap& uri_map, + const std::string& app, + const FilePath& filename, + unsigned scopes); + + /** Load files from the standard configuration directories for the app. + * + * The system configuration file(s), e.g. /etc/xdg/appname/filename, + * will be loaded before the user's, e.g. ~/.config/appname/filename, + * so the user options will override the system options. + */ + std::list<FilePath> load_default(const std::string& app, + const FilePath& filename); + + const Atom& option(const std::string& long_name) const; + bool set(const std::string& long_name, const Atom& value); + +private: + struct Option { + std::string key; + std::string name; + char letter; + std::string desc; + Scope scope; + LV2_URID type; + Atom value; + }; + + struct OptionNameOrder { + inline bool operator()(const Option& a, const Option& b) { + return a.name < b.name; + } + }; + + typedef std::map<std::string, Option> Options; + typedef std::map<char, std::string> ShortNames; + typedef std::map<std::string, std::string> Keys; + + std::string variable_string(LV2_URID type) const; + + int set_value_from_string(Configuration::Option& option, + const std::string& value); + + Forge& _forge; + const std::string _shortdesc; + const std::string _desc; + Options _options; + Keys _keys; + ShortNames _short_names; + size_t _max_name_length; +}; + +} // namespace Ingen + +#endif // INGEN_CONFIGURATION_HPP diff --git a/ingen/DataAccess.hpp b/ingen/DataAccess.hpp new file mode 100644 index 00000000..cabb5345 --- /dev/null +++ b/ingen/DataAccess.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_DATAACCESS_HPP +#define INGEN_ENGINE_DATAACCESS_HPP + +#include <cstdlib> +#include <utility> + +#include "ingen/LV2Features.hpp" +#include "ingen/Node.hpp" +#include "ingen/Store.hpp" +#include "ingen/World.hpp" +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "lv2/lv2plug.in/ns/ext/data-access/data-access.h" + +namespace Ingen { + +struct DataAccess : public Ingen::LV2Features::Feature +{ + static void delete_feature(LV2_Feature* feature) { + free(feature->data); + delete feature; + } + + const char* uri() const override { return "http://lv2plug.in/ns/ext/data-access"; } + + SPtr<LV2_Feature> feature(World* world, Node* node) override { + Node* store_node = world->store()->get(node->path()); + if (!store_node) { + return SPtr<LV2_Feature>(); + } + + LilvInstance* inst = store_node->instance(); + if (!inst) { + return SPtr<LV2_Feature>(); + } + + const LV2_Descriptor* desc = lilv_instance_get_descriptor(inst); + LV2_Extension_Data_Feature* data = (LV2_Extension_Data_Feature*) + malloc(sizeof(LV2_Extension_Data_Feature)); + + data->data_access = desc->extension_data; + + return SPtr<LV2_Feature>( + new LV2_Feature{ "http://lv2plug.in/ns/ext/data-access", data }); + } +}; + +} // namespace Ingen + +#endif // INGEN_ENGINE_DATAACCESS_HPP diff --git a/ingen/EngineBase.hpp b/ingen/EngineBase.hpp new file mode 100644 index 00000000..00436c66 --- /dev/null +++ b/ingen/EngineBase.hpp @@ -0,0 +1,145 @@ +/* + 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_ENGINEBASE_HPP +#define INGEN_ENGINEBASE_HPP + +#include <chrono> +#include <cstddef> +#include <cstdint> + +#include "ingen/ingen.h" +#include "ingen/types.hpp" + +namespace Ingen { + +class Interface; + +/** + The audio engine which executes the graph. + + @ingroup Ingen +*/ +class INGEN_API EngineBase +{ +public: + virtual ~EngineBase() = default; + + /** + Initialise the engine for local use (e.g. without a Jack driver). + @param sample_rate Audio sampling rate in Hz. + @param block_length Audio block length (i.e. buffer size) in frames. + @param seq_size Sequence buffer size in bytes. + */ + virtual void init(double sample_rate, + uint32_t block_length, + size_t seq_size) = 0; + + /** + Return true iff the engine and driver supports dynamic ports. + + This returns false in situations where top level ports can not be + created once the driver is running, which is the case for most + environments outside Jack. + */ + virtual bool supports_dynamic_ports() const = 0; + + /** + Activate the engine. + */ + virtual bool activate() = 0; + + /** + Deactivate the engine. + */ + virtual void deactivate() = 0; + + /** + Begin listening on network sockets. + */ + virtual void listen() = 0; + + /** + Return true iff events are waiting to be processed. + */ + virtual bool pending_events() const = 0; + + /** + Flush any pending events. + + This function is only safe to call in sequential contexts, and runs both + process thread and main iterations in lock-step. + + @param sleep_ms Interval in milliseconds to sleep between each block. + */ + virtual void flush_events(const std::chrono::milliseconds& sleep_ms) = 0; + + /** + Advance audio time by the given number of frames. + */ + virtual void advance(uint32_t nframes) = 0; + + /** + Locate to a given audio position. + */ + virtual void locate(uint32_t start, uint32_t sample_count) = 0; + + /** + Process audio for `sample_count` frames. + + If the return value is non-zero, events have been processed and are + awaiting to be finalised (including responding and announcing any changes + to clients) via a call to main_iteration(). + + @return The number of events processed. + */ + virtual unsigned run(uint32_t sample_count) = 0; + + /** + Indicate that a quit is desired. + + This function simply sets a flag which affects the return value of + main_iteration, it does not actually force the engine to stop running or + block. The code driving the engine is responsible for stopping and + cleaning up when main_iteration returns false. + */ + virtual void quit() = 0; + + /** + Run a single iteration of the main context. + + The main context post-processes events and performs housekeeping duties + like collecting garbage. This should be called regularly, e.g. a few + times per second. The return value indicates whether execution should + continue; i.e. if false is returned, a quit has been requested and the + caller should cease calling main_iteration() and stop the engine. + */ + virtual bool main_iteration() = 0; + + /** + Register a client to receive updates about engine changes. + */ + virtual void register_client(SPtr<Interface> client) = 0; + + /** + Unregister a client. + */ + virtual bool unregister_client(SPtr<Interface> client) = 0; +}; + +} // namespace Ingen + +#endif // INGEN_ENGINEBASE_HPP diff --git a/ingen/FilePath.hpp b/ingen/FilePath.hpp new file mode 100644 index 00000000..6bdd6044 --- /dev/null +++ b/ingen/FilePath.hpp @@ -0,0 +1,123 @@ +/* + 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/>. +*/ + +#ifndef INGEN_FILE_PATH_HPP +#define INGEN_FILE_PATH_HPP + +#include <iosfwd> +#include <type_traits> +#include <utility> + +#include <boost/utility/string_view.hpp> + +#if defined(_WIN32) && !defined(__CYGWIN__) +#define USE_WINDOWS_FILE_PATHS 1 +#endif + +namespace Ingen { + +/** A path to a file. + * + * This is a minimal subset of the std::filesystem::path interface in C++17. + * Support for Windows paths is only partial and there is no support for + * character encoding conversion at all. + */ +class FilePath +{ +public: +#ifdef USE_WINDOWS_FILE_PATHS + typedef wchar_t value_type; + static constexpr value_type preferred_separator = L'\\'; +#else + typedef char value_type; + static constexpr value_type preferred_separator = '/'; +#endif + + typedef std::basic_string<value_type> string_type; + + FilePath() = default; + FilePath(const FilePath&) = default; + FilePath(FilePath&&) = default; + + FilePath(string_type&& str) : _str(std::move(str)) {} + FilePath(const string_type& str) : _str(str) {} + FilePath(const value_type* str) : _str(str) {} + FilePath(const boost::basic_string_view<value_type>& sv) + : _str(sv.data(), sv.length()) + {} + + ~FilePath() = default; + + FilePath& operator=(const FilePath& path) = default; + FilePath& operator=(FilePath&& path) noexcept; + FilePath& operator=(string_type&& str); + + FilePath& operator/=(const FilePath& path); + + FilePath& operator+=(const FilePath& path); + FilePath& operator+=(const string_type& str); + FilePath& operator+=(const value_type* str); + FilePath& operator+=(value_type chr); + FilePath& operator+=(boost::basic_string_view<value_type> sv); + + void clear() noexcept { _str.clear(); } + + const string_type& native() const noexcept { return _str; } + const string_type& string() const noexcept { return _str; } + const value_type* c_str() const noexcept { return _str.c_str(); } + + operator string_type() const { return _str; } + + FilePath root_name() const; + FilePath root_directory() const; + FilePath root_path() const; + FilePath relative_path() const; + FilePath parent_path() const; + FilePath filename() const; + FilePath stem() const; + FilePath extension() const; + + bool empty() const noexcept { return _str.empty(); } + + bool is_absolute() const; + bool is_relative() const { return !is_absolute(); } + +private: + std::size_t find_first_sep() const; + std::size_t find_last_sep() const; + + string_type _str; +}; + +bool operator==(const FilePath& lhs, const FilePath& rhs) noexcept; +bool operator!=(const FilePath& lhs, const FilePath& rhs) noexcept; +bool operator<(const FilePath& lhs, const FilePath& rhs) noexcept; +bool operator<=(const FilePath& lhs, const FilePath& rhs) noexcept; +bool operator>(const FilePath& lhs, const FilePath& rhs) noexcept; +bool operator>=(const FilePath& lhs, const FilePath& rhs) noexcept; + +FilePath operator/(const FilePath& lhs, const FilePath& rhs); + +template <typename Char, typename Traits> +std::basic_ostream<Char, Traits>& +operator<<(std::basic_ostream<Char, Traits>& os, const FilePath& path) +{ + return os << path.string(); +} + +} // namespace Ingen + +#endif // INGEN_FILE_PATH_HPP diff --git a/ingen/Forge.hpp b/ingen/Forge.hpp new file mode 100644 index 00000000..32822f89 --- /dev/null +++ b/ingen/Forge.hpp @@ -0,0 +1,86 @@ +/* + 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_FORGE_HPP +#define INGEN_FORGE_HPP + +#include <cstdint> +#include <cstring> +#include <string> + +#include "ingen/Atom.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" + +namespace Ingen { + +class URIMap; +class URI; + +/** Forge for Atoms. + * @ingroup IngenShared + */ +class INGEN_API Forge : public LV2_Atom_Forge { +public: + explicit Forge(URIMap& map); + + std::string str(const Atom& atom, bool quoted); + + bool is_uri(const Atom& atom) const { + return atom.type() == URI || atom.type() == URID; + } + + Atom make() { return Atom(); } + Atom make(int32_t v) { return Atom(sizeof(v), Int, &v); } + Atom make(float v) { return Atom(sizeof(v), Float, &v); } + Atom make(bool v) { + const int32_t iv = v ? 1 : 0; + return Atom(sizeof(int32_t), Bool, &iv); + } + + Atom make_urid(int32_t v) { return Atom(sizeof(int32_t), URID, &v); } + + Atom make_urid(const Ingen::URI& u); + + Atom alloc(uint32_t size, uint32_t type, const void* val) { + return Atom(size, type, val); + } + + Atom alloc(const char* v) { + const size_t len = strlen(v); + return Atom(len + 1, String, v); + } + + Atom alloc(const std::string& v) { + return Atom(v.length() + 1, String, v.c_str()); + } + + Atom alloc_uri(const char* v) { + const size_t len = strlen(v); + return Atom(len + 1, URI, v); + } + + Atom alloc_uri(const std::string& v) { + return Atom(v.length() + 1, URI, v.c_str()); + } + +private: + URIMap& _map; +}; + +} // namespace Ingen + +#endif // INGEN_FORGE_HPP diff --git a/ingen/InstanceAccess.hpp b/ingen/InstanceAccess.hpp new file mode 100644 index 00000000..a93edc66 --- /dev/null +++ b/ingen/InstanceAccess.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_ENGINE_INSTANCEACCESS_HPP +#define INGEN_ENGINE_INSTANCEACCESS_HPP + +#include <utility> + +#include "ingen/LV2Features.hpp" +#include "ingen/Node.hpp" +#include "ingen/Store.hpp" +#include "ingen/World.hpp" +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +namespace Ingen { + +struct InstanceAccess : public Ingen::LV2Features::Feature +{ + const char* uri() const override { return "http://lv2plug.in/ns/ext/instance-access"; } + + SPtr<LV2_Feature> feature(World* world, Node* node) override { + Node* store_node = world->store()->get(node->path()); + if (!store_node) { + return SPtr<LV2_Feature>(); + } + + LilvInstance* instance = store_node->instance(); + if (!instance) { + return SPtr<LV2_Feature>(); + } + + return SPtr<LV2_Feature>( + new LV2_Feature{ "http://lv2plug.in/ns/ext/instance-access", + lilv_instance_get_handle(instance) }); + } +}; + +} // namespace Ingen + +#endif // INGEN_ENGINE_INSTANCEACCESS_HPP diff --git a/ingen/Interface.hpp b/ingen/Interface.hpp new file mode 100644 index 00000000..a6654afe --- /dev/null +++ b/ingen/Interface.hpp @@ -0,0 +1,149 @@ +/* + 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/>. +*/ + +/** + @defgroup Ingen Core Interfaces +*/ + +#ifndef INGEN_INTERFACE_HPP +#define INGEN_INTERFACE_HPP + +#include <cstdint> +#include <string> + +#include "ingen/Message.hpp" +#include "ingen/Properties.hpp" +#include "ingen/Resource.hpp" +#include "ingen/Status.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" + +namespace Raul { +class Path; +} + +namespace Ingen { + +class Atom; +class URI; + +/** Abstract interface for Ingen servers and clients. + * + * @ingroup Ingen + */ +class INGEN_API Interface +{ +public: + using result_type = void; + + Interface() : _seq(0) {} + + virtual ~Interface() = default; + + virtual URI uri() const = 0; + + virtual SPtr<Interface> respondee() const { return SPtr<Interface>(); } + + virtual void set_respondee(SPtr<Interface> respondee) {} + + virtual void message(const Message& msg) = 0; + + /** @name Convenience API + * @{ + */ + + inline void operator()(const Message& msg) { message(msg); } + + inline void set_response_id(int32_t id) { _seq = id; } + + inline void bundle_begin() { message(BundleBegin{_seq++}); } + inline void bundle_end() { message(BundleEnd{_seq++}); } + + inline void put(const URI& uri, + const Properties& properties, + Resource::Graph ctx = Resource::Graph::DEFAULT) + { + message(Put{_seq++, uri, properties, ctx}); + } + + inline void delta(const URI& uri, + const Properties& remove, + const Properties& add, + Resource::Graph ctx = Resource::Graph::DEFAULT) + { + message(Delta{_seq++, uri, remove, add, ctx}); + } + + inline void copy(const URI& old_uri, const URI& new_uri) + { + message(Copy{_seq++, old_uri, new_uri}); + } + + inline void move(const Raul::Path& old_path, const Raul::Path& new_path) + { + message(Move{_seq++, old_path, new_path}); + } + + inline void del(const URI& uri) { message(Del{_seq++, uri}); } + + inline void connect(const Raul::Path& tail, const Raul::Path& head) + { + message(Connect{_seq++, tail, head}); + } + + inline void disconnect(const Raul::Path& tail, const Raul::Path& head) + { + message(Disconnect{_seq++, tail, head}); + } + + inline void disconnect_all(const Raul::Path& graph, const Raul::Path& path) + { + message(DisconnectAll{_seq++, graph, path}); + } + + inline void set_property(const URI& subject, + const URI& predicate, + const Atom& value, + Resource::Graph ctx = Resource::Graph::DEFAULT) + { + message(SetProperty{_seq++, subject, predicate, value, ctx}); + } + + inline void undo() { message(Undo{_seq++}); } + + inline void redo() { message(Redo{_seq++}); } + + inline void get(const URI& uri) { message(Get{_seq++, uri}); } + + inline void response(int32_t id, Status status, const std::string& subject) + { + message(Response{id, status, subject}); + } + + inline void error(const std::string& error_message) + { + message(Error{_seq++, error_message}); + } + + /** @} */ + +private: + int32_t _seq; +}; + +} // namespace Ingen + +#endif // INGEN_INTERFACE_HPP diff --git a/ingen/LV2Features.hpp b/ingen/LV2Features.hpp new file mode 100644 index 00000000..9995ff47 --- /dev/null +++ b/ingen/LV2Features.hpp @@ -0,0 +1,95 @@ +/* + 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_LV2FEATURES_HPP +#define INGEN_LV2FEATURES_HPP + +#include <string> +#include <utility> +#include <vector> + +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "raul/Noncopyable.hpp" + +namespace Ingen { + +class Node; +class World; + +/** Features for use by LV2 plugins. + * @ingroup IngenShared + */ +class INGEN_API LV2Features { +public: + LV2Features(); + + class Feature { + public: + virtual ~Feature() = default; + + virtual const char* uri() const = 0; + + virtual SPtr<LV2_Feature> feature(World* world, + Node* block) = 0; + +protected: + static void free_feature(LV2_Feature* feature); + }; + + class EmptyFeature : public Feature { + public: + explicit EmptyFeature(const char* uri) : _uri(uri) {} + + const char* uri() const override { return _uri; } + + SPtr<LV2_Feature> feature(World* world, Node* block) override { + return SPtr<LV2_Feature>(); + } + + const char* _uri; + }; + + class FeatureArray : public Raul::Noncopyable { + public: + typedef std::vector< SPtr<LV2_Feature> > FeatureVector; + + explicit FeatureArray(FeatureVector& features); + + ~FeatureArray(); + + LV2_Feature** array() { return _array; } + + private: + FeatureVector _features; + LV2_Feature** _array; + }; + + void add_feature(SPtr<Feature> feature); + bool is_supported(const std::string& uri) const; + + SPtr<FeatureArray> lv2_features(World* world, + Node* node) const; + +private: + typedef std::vector< SPtr<Feature> > Features; + Features _features; +}; + +} // namespace Ingen + +#endif // INGEN_LV2FEATURES_HPP diff --git a/ingen/Library.hpp b/ingen/Library.hpp new file mode 100644 index 00000000..71257900 --- /dev/null +++ b/ingen/Library.hpp @@ -0,0 +1,48 @@ +/* + 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/>. +*/ + +#ifndef INGEN_LIBRARY_HPP +#define INGEN_LIBRARY_HPP + +#include "ingen/FilePath.hpp" +#include "ingen/ingen.h" + +namespace Ingen { + +/** A dynamically loaded library (module, plugin). */ +class INGEN_API Library { +public: + Library(const FilePath& path); + ~Library(); + + Library(const Library&) = delete; + Library& operator=(const Library&) = delete; + + using VoidFuncPtr = void (*)(void); + + VoidFuncPtr get_function(const char* const name); + + static const char* get_last_error(); + + operator bool() const { return _lib; } + +private: + void* _lib; +}; + +} // namespace Ingen + +#endif // INGEN_LIBRARY_HPP diff --git a/ingen/Log.hpp b/ingen/Log.hpp new file mode 100644 index 00000000..54369551 --- /dev/null +++ b/ingen/Log.hpp @@ -0,0 +1,88 @@ +/* + 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_LOG_HPP +#define INGEN_LOG_HPP + +#include <cstdarg> +#include <cstdio> +#include <functional> +#include <string> + +#include <boost/format.hpp> + +#include "ingen/LV2Features.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +namespace Ingen { + +typedef boost::basic_format<char> fmt; + +class Node; +class URIs; +class World; + +class INGEN_API Log { +public: + typedef std::function<int(LV2_URID, const char*, va_list)> Sink; + + Log(LV2_Log_Log* log, URIs& uris); + + struct Feature : public LV2Features::Feature { + const char* uri() const override { return LV2_LOG__log; } + + SPtr<LV2_Feature> feature(World* world, Node* block) override; + + struct Handle { + LV2_Log_Log lv2_log; + Log* log; + Node* node; + }; + }; + + void rt_error(const char* msg); + + void error(const std::string& msg); + void info(const std::string& msg); + void warn(const std::string& msg); + void trace(const std::string& msg); + + inline void error(const fmt& fmt) { error(fmt.str()); } + inline void info(const fmt& fmt) { info(fmt.str()); } + inline void warn(const fmt& fmt) { warn(fmt.str()); } + + int vtprintf(LV2_URID type, const char* fmt, va_list args); + + void set_flush(bool f) { _flush = f; } + void set_trace(bool f) { _trace = f; } + void set_sink(Sink s) { _sink = s; } + +private: + void print(FILE* stream, const std::string& msg); + + LV2_Log_Log* _log; + URIs& _uris; + Sink _sink; + bool _flush; + bool _trace; +}; + +} // namespace Ingen + +#endif // INGEN_LOG_HPP diff --git a/ingen/Message.hpp b/ingen/Message.hpp new file mode 100644 index 00000000..515a2e1f --- /dev/null +++ b/ingen/Message.hpp @@ -0,0 +1,158 @@ +/* + 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_MESSAGE_HPP +#define INGEN_MESSAGE_HPP + +#include <cstdint> +#include <string> + +#include <boost/variant/variant.hpp> + +#include "ingen/Atom.hpp" +#include "ingen/Properties.hpp" +#include "ingen/Resource.hpp" +#include "ingen/Status.hpp" +#include "raul/Path.hpp" + +namespace Ingen { + +struct BundleBegin +{ + int32_t seq; +}; + +struct BundleEnd +{ + int32_t seq; +}; + +struct Connect +{ + int32_t seq; + Raul::Path tail; + Raul::Path head; +}; + +struct Copy +{ + int32_t seq; + URI old_uri; + URI new_uri; +}; + +struct Del +{ + int32_t seq; + URI uri; +}; + +struct Delta +{ + int32_t seq; + URI uri; + Properties remove; + Properties add; + Resource::Graph ctx; +}; + +struct Disconnect +{ + int32_t seq; + Raul::Path tail; + Raul::Path head; +}; + +struct DisconnectAll +{ + int32_t seq; + Raul::Path graph; + Raul::Path path; +}; + +struct Error +{ + int32_t seq; + std::string message; +}; + +struct Get +{ + int32_t seq; + URI subject; +}; + +struct Move +{ + int32_t seq; + Raul::Path old_path; + Raul::Path new_path; +}; + +struct Put +{ + int32_t seq; + URI uri; + Properties properties; + Resource::Graph ctx; +}; + +struct Redo +{ + int32_t seq; +}; + +struct Response +{ + int32_t id; + Status status; + std::string subject; +}; + +struct SetProperty +{ + int32_t seq; + URI subject; + URI predicate; + Atom value; + Resource::Graph ctx; +}; + +struct Undo +{ + int32_t seq; +}; + +using Message = boost::variant<BundleBegin, + BundleEnd, + Connect, + Copy, + Del, + Delta, + Disconnect, + DisconnectAll, + Error, + Get, + Move, + Put, + Redo, + Response, + SetProperty, + Undo>; + +} // namespace Ingen + +#endif // INGEN_MESSAGE_HPP diff --git a/ingen/Module.hpp b/ingen/Module.hpp new file mode 100644 index 00000000..df47b70b --- /dev/null +++ b/ingen/Module.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_MODULE_HPP +#define INGEN_MODULE_HPP + +#include <memory> + +#include "ingen/FilePath.hpp" +#include "ingen/Library.hpp" +#include "ingen/ingen.h" + +namespace Ingen { + +class World; + +/** A dynamically loaded Ingen module. + * + * All components of Ingen reside in one of these. + * @ingroup IngenShared + */ +class INGEN_API Module { +public: + Module() : library(nullptr) {} + virtual ~Module() = default; + + Module(const Module&) = delete; + Module& operator=(const Module&) = delete; + + virtual void load(Ingen::World* world) = 0; + virtual void run(Ingen::World* world) {} + + /** Library implementing this module. + * + * This is managed by the World and not this class, since closing the library + * in this destructor could possibly reference code from the library + * afterwards and cause a segfault on exit. + */ + std::unique_ptr<Library> library; +}; + +} // namespace Ingen + +extern "C" { + +/** Prototype for the ingen_module_load() entry point in an ingen module. */ +INGEN_API Ingen::Module* ingen_module_load(); + +} + +#endif // INGEN_MODULE_HPP diff --git a/ingen/Node.hpp b/ingen/Node.hpp new file mode 100644 index 00000000..ca78aa3d --- /dev/null +++ b/ingen/Node.hpp @@ -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/>. +*/ + +#ifndef INGEN_NODE_HPP +#define INGEN_NODE_HPP + +#include <cstdint> +#include <map> +#include <string> +#include <utility> + +#include "ingen/Resource.hpp" +#include "ingen/ingen.h" +#include "ingen/paths.hpp" +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +namespace Raul { +class Path; +class Symbol; +} + +namespace Ingen { + +class Arc; +class FilePath; +class Store; +class URIs; + +/** A node in the audio graph. + * + * The key property of nodes is that all nodes have a path and a symbol, as + * well as a URI. + * + * To avoid ugly inheritance issues and the need for excessive use of + * dynamic_cast, this class contains some members which are only applicable to + * certain types of node. There is a type tag which can be used to determine + * the type of any Node. + * + * @ingroup Ingen + */ +class INGEN_API Node : public Resource +{ +public: + enum class GraphType { + GRAPH, + BLOCK, + PORT + }; + + typedef std::pair<const Node*, const Node*> ArcsKey; + typedef std::map< ArcsKey, SPtr<Arc> > Arcs; + + // Graphs only + Arcs& arcs() { return _arcs; } + const Arcs& arcs() const { return _arcs; } + + // Blocks and graphs only + virtual uint32_t num_ports() const { return 0; } + virtual Node* port(uint32_t index) const { return nullptr; } + virtual const Resource* plugin() const { return nullptr; } + + // Plugin blocks only + virtual LilvInstance* instance() { return nullptr; } + virtual bool save_state(const FilePath& dir) const { return false; } + + // All objects + virtual GraphType graph_type() const = 0; + virtual const Raul::Path& path() const = 0; + virtual const Raul::Symbol& symbol() const = 0; + virtual Node* graph_parent() const = 0; + + URI base_uri() const { + if (uri().string()[uri().string().size() - 1] == '/') { + return uri(); + } + return URI(uri().string() + '/'); + } + +protected: + friend class Store; + virtual void set_path(const Raul::Path& p) = 0; + + Node(const URIs& uris, const Raul::Path& path) + : Resource(uris, path_to_uri(path)) + {} + + Arcs _arcs; ///< Graphs only +}; + +} // namespace Ingen + +#endif // INGEN_NODE_HPP diff --git a/ingen/Parser.hpp b/ingen/Parser.hpp new file mode 100644 index 00000000..5a361cd3 --- /dev/null +++ b/ingen/Parser.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_PARSER_HPP +#define INGEN_PARSER_HPP + +#include <set> +#include <string> +#include <utility> + +#include <boost/optional/optional.hpp> + +#include "ingen/FilePath.hpp" +#include "ingen/Properties.hpp" +#include "ingen/URI.hpp" +#include "ingen/ingen.h" +#include "raul/Path.hpp" +#include "raul/Symbol.hpp" + +namespace Sord { class World; } + +namespace Ingen { + +class Interface; +class World; + +/** + Parser for reading graphs from Turtle files or strings. + + @ingroup Ingen +*/ +class INGEN_API Parser { +public: + explicit Parser() = default; + + virtual ~Parser() = default; + + /** Record of a resource listed in a bundle manifest. */ + struct ResourceRecord { + inline ResourceRecord(URI u, FilePath f) + : uri(std::move(u)), filename(std::move(f)) + {} + + inline bool operator<(const ResourceRecord& r) const { + return uri < r.uri; + } + + URI uri; ///< URI of resource (e.g. a Graph) + FilePath filename; ///< Path of describing file (seeAlso) + }; + + /** Find all resources of a given type listed in a manifest file. */ + virtual std::set<ResourceRecord> find_resources(Sord::World& world, + const URI& manifest_uri, + const URI& type_uri); + + /** Parse a graph from RDF into a Interface (engine or client). + * + * If `path` is a file path, then the graph is loaded from that + * file. If it is a directory, then the manifest.ttl from that directory + * is used instead. In either case, any rdfs:seeAlso files are loaded and + * the graph parsed from the resulting combined model. + * + * @return whether or not load was successful. + */ + virtual bool parse_file( + World* world, + Interface* target, + const FilePath& path, + 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>()); + + virtual boost::optional<URI> parse_string( + World* world, + Interface* target, + const std::string& str, + const URI& base_uri, + 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>()); +}; + +} // namespace Ingen + +#endif // INGEN_PARSER_HPP diff --git a/ingen/Properties.hpp b/ingen/Properties.hpp new file mode 100644 index 00000000..e148c542 --- /dev/null +++ b/ingen/Properties.hpp @@ -0,0 +1,90 @@ +/* + 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_PROPERTIES_HPP +#define INGEN_PROPERTIES_HPP + +#include <initializer_list> +#include <map> +#include <utility> + +#include "ingen/Atom.hpp" +#include "ingen/URIs.hpp" + +namespace Ingen { + +/** A property value (an Atom with a context). */ +class Property : public Atom { +public: + enum class Graph { + DEFAULT, ///< Default context for "universal" properties + EXTERNAL, ///< Externally visible graph properties + INTERNAL ///< Internally visible graph properties + }; + + Property(const Atom& atom, Graph ctx=Graph::DEFAULT) + : Atom(atom) + , _ctx(ctx) + {} + + Property(const URIs::Quark& quark, Graph ctx=Graph::DEFAULT) + : Atom(quark.urid) + , _ctx(ctx) + {} + + Graph context() const { return _ctx; } + void set_context(Graph ctx) { _ctx = ctx; } + +private: + Graph _ctx; +}; + +class Properties : public std::multimap<URI, Property> { +public: + using Graph = Property::Graph; + + Properties() = default; + Properties(const Properties& copy) = default; + + Properties(std::initializer_list<value_type> l) + : std::multimap<URI, Property>(l) + {} + + void put(const URI& key, + const Atom& value, + Graph ctx = Graph::DEFAULT) { + emplace(key, Property(value, ctx)); + } + + void put(const URI& key, + const URIs::Quark& value, + Graph ctx = Graph::DEFAULT) { + emplace(key, Property(value, ctx)); + } + + bool contains(const URI& key, const Atom& value) { + for (const_iterator i = find(key); i != end() && i->first == key; ++i) { + if (i->second == value) { + return true; + } + } + return false; + } +}; + +} // namespace Ingen + +#endif // INGEN_PROPERTIES_HPP diff --git a/ingen/QueuedInterface.hpp b/ingen/QueuedInterface.hpp new file mode 100644 index 00000000..bf424edd --- /dev/null +++ b/ingen/QueuedInterface.hpp @@ -0,0 +1,66 @@ +/* + 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/>. +*/ + +#ifndef INGEN_ENGINE_QUEUEDINTERFACE_HPP +#define INGEN_ENGINE_QUEUEDINTERFACE_HPP + +#include <mutex> +#include <vector> + +#include "ingen/Interface.hpp" +#include "ingen/Message.hpp" + +namespace Ingen { + +/** Stores all messages and emits them to a sink on demand. + * + * This can be used to make an interface thread-safe. + */ +class QueuedInterface : public Interface +{ +public: + explicit QueuedInterface(SPtr<Interface> sink) : _sink(std::move(sink)) {} + + URI uri() const override { return URI("ingen:/QueuedInterface"); } + + void message(const Message& message) override { + std::lock_guard<std::mutex> lock(_mutex); + _messages.emplace_back(message); + } + + void emit() { + std::vector<Message> messages; + { + std::lock_guard<std::mutex> lock(_mutex); + _messages.swap(messages); + } + + for (const auto& i : messages) { + _sink->message(i); + } + } + + const SPtr<Interface>& sink() const { return _sink; } + +private: + std::mutex _mutex; + SPtr<Interface> _sink; + std::vector<Message> _messages; +}; + +} // namespace Ingen + +#endif // INGEN_ENGINE_QUEUEDINTERFACE_HPP diff --git a/ingen/Resource.hpp b/ingen/Resource.hpp new file mode 100644 index 00000000..9fc854c7 --- /dev/null +++ b/ingen/Resource.hpp @@ -0,0 +1,205 @@ +/* + 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_RESOURCE_HPP +#define INGEN_RESOURCE_HPP + +#include <cassert> +#include <string> + +#include "ingen/Properties.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIs.hpp" +#include "ingen/ingen.h" +#include "raul/Deletable.hpp" + +namespace Ingen { + +class Atom; + +/** A resource with a URI described by properties. + * + * This is the base class for most things in Ingen, including graphs, blocks, + * ports, and the engine itself. + * + * @ingroup Ingen + */ +class INGEN_API Resource : public Raul::Deletable +{ +public: + using Graph = Property::Graph; + + Resource(const URIs& uris, const URI& uri) + : _uris(uris) + , _uri(uri) + {} + + Resource& operator=(const Resource& rhs) { + assert(&rhs._uris == &_uris); + if (&rhs != this) { + _uri = rhs._uri; + _properties = rhs._properties; + } + return *this; + } + + static URI graph_to_uri(Graph g) { + switch (g) { + case Graph::EXTERNAL: return URI(INGEN_NS "externalContext"); + case Graph::INTERNAL: return URI(INGEN_NS "internalContext"); + default: return URI(INGEN_NS "defaultContext"); + } + } + + static Graph uri_to_graph(const URI& uri) { + if (uri == INGEN_NS "externalContext") { + return Graph::EXTERNAL; + } else if (uri == INGEN_NS "internalContext") { + return Graph::INTERNAL; + } + return Graph::DEFAULT; + } + + /** Get a single property value. + * + * This is only useful for properties with a single value. If the + * requested property has several values, the first will be returned. + */ + virtual const Atom& get_property(const URI& uri) const; + + /** Set (replace) a property value. + * + * This will first erase any properties with the given `uri`, so after + * this call exactly one property with predicate `uri` will be set. + */ + virtual const Atom& set_property(const URI& uri, + const Atom& value, + Graph ctx = Graph::DEFAULT); + + /** Set (replace) a property value. + * + * This will first erase any properties with the given `uri`, so after + * this call exactly one property with predicate `uri` will be set. + */ + virtual const Atom& set_property(const URI& uri, + const URIs::Quark& value, + Graph ctx = Graph::DEFAULT); + + /** Add a property value. + * + * This will not remove any existing values, so if properties with + * predicate `uri` and values other than `value` exist, this will result + * in multiple values for the property. + * + * @return True iff a new property was added. + */ + virtual bool add_property(const URI& uri, + const Atom& value, + Graph ctx = Graph::DEFAULT); + + /** Remove a property. + * + * If `value` is patch:wildcard then any property with `uri` for a + * predicate will be removed. + */ + virtual void remove_property(const URI& uri, + const Atom& value); + + /** Remove a property. + * + * If `value` is patch:wildcard then any property with `uri` for a + * predicate will be removed. + */ + virtual void remove_property(const URI& uri, + const URIs::Quark& value); + + /** Return true iff a property is set with a specific value. */ + virtual bool has_property(const URI& uri, + const Atom& value) const; + + /** Return true iff a property is set with a specific value. */ + virtual bool has_property(const URI& uri, + const URIs::Quark& value) const; + + /** Set (replace) several properties at once. + * + * This will erase all properties with keys in `p`, though multiple values + * for one property may exist in `p` will all be set (unlike simply + * calling set_property in a loop which would only set one value). + */ + void set_properties(const Properties& props); + + /** Add several properties at once. */ + void add_properties(const Properties& props); + + /** Remove several properties at once. + * + * This removes all matching properties (both key and value), or all + * properties with a matching key if the value in `p` is patch:wildcard. + */ + void remove_properties(const Properties& props); + + /** Hook called whenever a property is added. + * + * This can be used by derived classes to implement special behaviour for + * particular properties (e.g. ingen:value for ports). + */ + virtual void on_property(const URI& uri, const Atom& value) {} + + /** Hook called whenever a property value is removed. + * + * If all values for a key are removed, then value will be the wildcard. + * + * This can be used by derived classes to implement special behaviour for + * particular properties (e.g. ingen:value for ports). + */ + virtual void on_property_removed(const URI& uri, const Atom& value) {} + + /** Get the ingen type from a set of Properties. + * + * If some coherent ingen type is found, true is returned and the appropriate + * output parameter set to true. Otherwise false is returned. + */ + static bool type(const URIs& uris, + const Properties& properties, + bool& graph, + bool& block, + bool& port, + bool& is_output); + + virtual void set_uri(const URI& uri) { _uri = uri; } + + /** Get all the properties with a given context. */ + Properties properties(Resource::Graph ctx) const; + + const URIs& uris() const { return _uris; } + const URI& uri() const { return _uri; } + const Properties& properties() const { return _properties; } + Properties& properties() { return _properties; } + +protected: + const Atom& set_property(const URI& uri, const Atom& value) const; + + const URIs& _uris; + +private: + URI _uri; + mutable Properties _properties; +}; + +} // namespace Ingen + +#endif // INGEN_RESOURCE_HPP diff --git a/ingen/Serialiser.hpp b/ingen/Serialiser.hpp new file mode 100644 index 00000000..fcfb40a7 --- /dev/null +++ b/ingen/Serialiser.hpp @@ -0,0 +1,103 @@ +/* + 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_SERIALISER_HPP +#define INGEN_SERIALISER_HPP + +#include <stdexcept> +#include <string> + +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "sord/sordmm.hpp" + +namespace Raul { class Path; } + +namespace Ingen { + +class Arc; +class Node; +class URI; +class World; + +/** + Serialiser for writing graphs to Turtle files or strings. + + @ingroup Ingen +*/ +class INGEN_API Serialiser +{ +public: + explicit Serialiser(World& world); + virtual ~Serialiser(); + + /** Write a graph and all its contents as a complete bundle. */ + virtual void write_bundle(SPtr<const Node> graph, + const URI& uri); + + /** Begin a serialization to a string. + * + * This must be called before any serializing methods. + * + * The results of the serialization will be returned by the finish() method after + * the desired objects have been serialised. + * + * All serialized paths will have the root path chopped from their prefix + * (therefore all serialized paths must be descendants of the root) + */ + virtual void start_to_string(const Raul::Path& root, + const URI& base_uri); + + /** Begin a serialization to a file. + * + * This must be called before any serializing methods. + * + * All serialized paths will have the root path chopped from their prefix + * (therefore all serialized paths must be descendants of the root) + */ + virtual void start_to_file(const Raul::Path& root, + const std::string& filename); + + /** Serialize an object (graph, block, or port). + * + * @throw std::logic_error + */ + virtual void serialise(SPtr<const Node> object); + + /** Serialize an arc. + * + * @throw std::logic_error + */ + virtual void serialise_arc(const Sord::Node& parent, + SPtr<const Arc> arc); + + /** Finish serialization. + * + * If this is a file serialization, this must be called to finish and close + * the output file, and the empty string is returned. + * + * If this is a string serialization, the serialized result is returned. + */ + virtual std::string finish(); + +private: + struct Impl; + Impl* me; +}; + +} // namespace Ingen + +#endif // INGEN_SERIALISER_HPP diff --git a/ingen/SocketReader.hpp b/ingen/SocketReader.hpp new file mode 100644 index 00000000..37ec8567 --- /dev/null +++ b/ingen/SocketReader.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_SOCKET_READER_HPP +#define INGEN_SOCKET_READER_HPP + +#include <thread> + +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "serd/serd.h" +#include "sord/sord.h" + +namespace Raul { class Socket; } + +namespace Ingen { + +class Interface; +class World; + +/** Calls Interface methods based on Turtle messages received via socket. */ +class INGEN_API SocketReader +{ +public: + SocketReader(World& world, + Interface& iface, + SPtr<Raul::Socket> sock); + + virtual ~SocketReader(); + +protected: + virtual void on_hangup() {} + +private: + void run(); + + static SerdStatus set_base_uri(SocketReader* iface, + const SerdNode* uri_node); + + static SerdStatus set_prefix(SocketReader* iface, + const SerdNode* name, + const SerdNode* uri_node); + + static SerdStatus 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); + + World& _world; + Interface& _iface; + SerdEnv* _env; + SordInserter* _inserter; + SordNode* _msg_node; + SPtr<Raul::Socket> _socket; + bool _exit_flag; + std::thread _thread; +}; + +} // namespace Ingen + +#endif // INGEN_SOCKET_READER_HPP diff --git a/ingen/SocketWriter.hpp b/ingen/SocketWriter.hpp new file mode 100644 index 00000000..1dcc077b --- /dev/null +++ b/ingen/SocketWriter.hpp @@ -0,0 +1,57 @@ +/* + This file is part of Ingen. + Copyright 2012-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_SOCKET_WRITER_HPP +#define INGEN_SOCKET_WRITER_HPP + +#include <cstddef> +#include <cstdint> + +#include "ingen/TurtleWriter.hpp" +#include "ingen/ingen.h" + +namespace Raul { +class Socket; +} + +namespace Ingen { + +class URI; +class URIMap; +class URIs; + +/** An Interface that writes Turtle messages to a socket. + */ +class INGEN_API SocketWriter : public TurtleWriter +{ +public: + SocketWriter(URIMap& map, + URIs& uris, + const URI& uri, + SPtr<Raul::Socket> sock); + + size_t text_sink(const void* buf, size_t len) override; + + /** Override of bundle_end to terminate bundles in the stream. */ + void bundle_end(); + +protected: + SPtr<Raul::Socket> _socket; +}; + +} // namespace Ingen + +#endif // INGEN_SOCKET_WRITER_HPP diff --git a/ingen/Status.hpp b/ingen/Status.hpp new file mode 100644 index 00000000..c1002a17 --- /dev/null +++ b/ingen/Status.hpp @@ -0,0 +1,92 @@ +/* + 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_STATUS_HPP +#define INGEN_STATUS_HPP + +namespace Ingen { + +enum class Status { + SUCCESS, + FAILURE, + + BAD_INDEX, + BAD_OBJECT_TYPE, + BAD_REQUEST, + BAD_URI, + BAD_VALUE_TYPE, + BAD_VALUE, + CLIENT_NOT_FOUND, + CREATION_FAILED, + DIRECTION_MISMATCH, + EXISTS, + INTERNAL_ERROR, + INVALID_PARENT, + INVALID_POLY, + NOT_DELETABLE, + NOT_FOUND, + NOT_MOVABLE, + NOT_PREPARED, + NO_SPACE, + PARENT_DIFFERS, + PARENT_NOT_FOUND, + PROTOTYPE_NOT_FOUND, + PORT_NOT_FOUND, + TYPE_MISMATCH, + UNKNOWN_TYPE, + COMPILATION_FAILED +}; + +static inline const char* +ingen_status_string(Status st) +{ + switch (st) { + case Status::SUCCESS: return "Success"; + case Status::FAILURE: return "Failure"; + + case Status::BAD_INDEX: return "Invalid index"; + case Status::BAD_OBJECT_TYPE: return "Invalid object type"; + case Status::BAD_REQUEST: return "Invalid request"; + case Status::BAD_URI: return "Invalid URI"; + case Status::BAD_VALUE_TYPE: return "Invalid value type"; + case Status::BAD_VALUE: return "Invalid value"; + case Status::CLIENT_NOT_FOUND: return "Client not found"; + case Status::CREATION_FAILED: return "Creation failed"; + case Status::DIRECTION_MISMATCH: return "Direction mismatch"; + case Status::EXISTS: return "Object exists"; + case Status::INTERNAL_ERROR: return "Internal error"; + case Status::INVALID_PARENT: return "Invalid parent"; + case Status::INVALID_POLY: return "Invalid polyphony"; + case Status::NOT_DELETABLE: return "Object not deletable"; + case Status::NOT_FOUND: return "Object not found"; + case Status::NOT_MOVABLE: return "Object not movable"; + case Status::NOT_PREPARED: return "Not prepared"; + case Status::NO_SPACE: return "Insufficient space"; + case Status::PARENT_DIFFERS: return "Parent differs"; + case Status::PARENT_NOT_FOUND: return "Parent not found"; + case Status::PROTOTYPE_NOT_FOUND: return "Prototype not found"; + case Status::PORT_NOT_FOUND: return "Port not found"; + case Status::TYPE_MISMATCH: return "Type mismatch"; + case Status::UNKNOWN_TYPE: return "Unknown type"; + case Status::COMPILATION_FAILED: return "Graph compilation failed"; + } + + return "Unknown error"; +} + +} // namespace Ingen + +#endif // INGEN_STATUS_HPP diff --git a/ingen/Store.hpp b/ingen/Store.hpp new file mode 100644 index 00000000..52c9012b --- /dev/null +++ b/ingen/Store.hpp @@ -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/>. +*/ + +#ifndef INGEN_STORE_HPP +#define INGEN_STORE_HPP + +#include <cstddef> +#include <map> +#include <mutex> +#include <utility> + +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "raul/Deletable.hpp" +#include "raul/Noncopyable.hpp" +#include "raul/Path.hpp" + +namespace Raul { class Symbol; } + +namespace Ingen { + +class Node; + +/** Store of objects in the graph hierarchy. + * @ingroup IngenShared + */ +class INGEN_API Store : public Raul::Noncopyable + , public Raul::Deletable + , public std::map< const Raul::Path, SPtr<Node> > { +public: + void add(Node* o); + + Node* get(const Raul::Path& path) { + const iterator i = find(path); + return (i == end()) ? nullptr : i->second.get(); + } + + typedef std::pair<const_iterator, const_iterator> const_range; + + typedef std::map< Raul::Path, SPtr<Node> > Objects; + + iterator find_descendants_end(Store::iterator parent); + const_iterator find_descendants_end(Store::const_iterator parent) const; + + const_range children_range(SPtr<const Node> o) const; + + /** Remove the object at `top` and all its children from the store. + * + * @param top Iterator to first (topmost parent) object to remove. + * + * @param removed Filled with all the removed objects. Note this may be + * many objects since any children will be removed as well. + */ + void remove(iterator top, Objects& removed); + + /** Rename (move) the object at `top` to `new_path`. + * + * Note this invalidates `i`. + */ + void rename(iterator top, const Raul::Path& new_path); + + unsigned child_name_offset(const Raul::Path& parent, + const Raul::Symbol& symbol, + bool allow_zero=true) const; + + typedef std::recursive_mutex Mutex; + + Mutex& mutex() { return _mutex; } + +private: + Mutex _mutex; +}; + +} // namespace Ingen + +#endif // INGEN_STORE_HPP diff --git a/ingen/StreamWriter.hpp b/ingen/StreamWriter.hpp new file mode 100644 index 00000000..66a03216 --- /dev/null +++ b/ingen/StreamWriter.hpp @@ -0,0 +1,52 @@ +/* + 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/>. +*/ + +#ifndef INGEN_STREAMWRITER_HPP +#define INGEN_STREAMWRITER_HPP + +#include <cstdio> + +#include "ingen/ingen.h" +#include "ingen/ColorContext.hpp" +#include "ingen/TurtleWriter.hpp" + +namespace Ingen { + +class URI; +class URIMap; +class URIs; + +/** An Interface that writes Turtle messages to a stream. + */ +class INGEN_API StreamWriter : public TurtleWriter +{ +public: + StreamWriter(URIMap& map, + URIs& uris, + const URI& uri, + FILE* stream, + ColorContext::Color color); + + size_t text_sink(const void* buf, size_t len) override; + +protected: + FILE* _stream; + ColorContext::Color _color; +}; + +} // namespace Ingen + +#endif // INGEN_STREAMWRITER_HPP diff --git a/ingen/Tee.hpp b/ingen/Tee.hpp new file mode 100644 index 00000000..2a6f00df --- /dev/null +++ b/ingen/Tee.hpp @@ -0,0 +1,63 @@ +/* + 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_TEE_HPP +#define INGEN_ENGINE_TEE_HPP + +#include <cstddef> +#include <mutex> +#include <set> +#include <utility> + +#include "ingen/Interface.hpp" +#include "ingen/Message.hpp" +#include "ingen/types.hpp" + +namespace Ingen { + +/** Interface that forwards all calls to several sinks. */ +class Tee : public Interface +{ +public: + typedef std::set< SPtr<Interface> > Sinks; + + explicit Tee(Sinks sinks) : _sinks(std::move(sinks)) {} + + SPtr<Interface> respondee() const override { + return (*_sinks.begin())->respondee(); + } + + void set_respondee(SPtr<Interface> respondee) override { + (*_sinks.begin())->set_respondee(respondee); + } + + void message(const Message& message) override { + std::lock_guard<std::mutex> lock(_sinks_mutex); + for (const auto& s : _sinks) { + s->message(message); + } + } + + URI uri() const override { return URI("ingen:/tee"); } + +private: + std::mutex _sinks_mutex; + Sinks _sinks; +}; + +} // namespace Ingen + +#endif // INGEN_ENGINE_TEE_HPP diff --git a/ingen/TurtleWriter.hpp b/ingen/TurtleWriter.hpp new file mode 100644 index 00000000..8cf7ceb8 --- /dev/null +++ b/ingen/TurtleWriter.hpp @@ -0,0 +1,68 @@ +/* + 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/>. +*/ + +#ifndef INGEN_TURTLE_WRITER_HPP +#define INGEN_TURTLE_WRITER_HPP + +#include <cstddef> +#include <cstdint> + +#include "ingen/AtomSink.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "serd/serd.h" +#include "sratom/sratom.h" + +namespace Ingen { + +class URIMap; +class URIs; + +/** An Interface that writes Turtle messages to a sink method. + * + * Derived classes must implement text_sink() to do something with the + * serialized messages. + */ +class INGEN_API TurtleWriter : public AtomWriter, public AtomSink +{ +public: + TurtleWriter(URIMap& map, URIs& uris, const URI& uri); + + ~TurtleWriter() override; + + /** AtomSink method which receives calls serialized to LV2 atoms. */ + bool write(const LV2_Atom* msg, int32_t default_id=0) override; + + /** Pure virtual text sink which receives calls serialized to Turtle. */ + virtual size_t text_sink(const void* buf, size_t len) = 0; + + URI uri() const override { return _uri; } + +protected: + URIMap& _map; + Sratom* _sratom; + SerdNode _base; + SerdURI _base_uri; + SerdEnv* _env; + SerdWriter* _writer; + URI _uri; + bool _wrote_prefixes; +}; + +} // namespace Ingen + +#endif // INGEN_TURTLE_WRITER_HPP diff --git a/ingen/URI.hpp b/ingen/URI.hpp new file mode 100644 index 00000000..3c6d38d1 --- /dev/null +++ b/ingen/URI.hpp @@ -0,0 +1,160 @@ +/* + 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/>. +*/ + +#ifndef INGEN_URI_HPP +#define INGEN_URI_HPP + +#include <iosfwd> +#include <string> + +#include <boost/utility/string_view.hpp> + +#include "ingen/FilePath.hpp" +#include "ingen/ingen.h" +#include "serd/serd.h" +#include "sord/sordmm.hpp" + +namespace Ingen { + +class INGEN_API URI +{ +public: + using Chunk = boost::string_view; + + URI(); + explicit URI(const std::string& str); + explicit URI(const char* str); + URI(const std::string& str, const URI& base); + URI(const Sord::Node& node); + URI(SerdNode node); + explicit URI(const FilePath& path); + + URI(const URI& uri); + URI& operator=(const URI& uri); + + URI(URI&& uri); + URI& operator=(URI&& uri); + + ~URI(); + + URI make_relative(const URI& base) const; + + bool empty() const { return !_node.buf; } + + std::string string() const { return std::string(c_str(), _node.n_bytes); } + size_t length() const { return _node.n_bytes; } + const char* c_str() const { return (const char*)_node.buf; } + + FilePath file_path() const { + return scheme() == "file" ? FilePath(path()) : FilePath(); + } + + operator std::string() const { return string(); } + + const char* begin() const { return (const char*)_node.buf; } + const char* end() const { return (const char*)_node.buf + _node.n_bytes; } + + Chunk scheme() const { return make_chunk(_uri.scheme); } + Chunk authority() const { return make_chunk(_uri.authority); } + Chunk path() const { return make_chunk(_uri.path); } + Chunk query() const { return make_chunk(_uri.query); } + Chunk fragment() const { return make_chunk(_uri.fragment); } + + static bool is_valid(const char* str) { + return serd_uri_string_has_scheme((const uint8_t*)str); + } + + static bool is_valid(const std::string& str) + { + return is_valid(str.c_str()); + } + +private: + URI(SerdNode node, SerdURI uri); + + static Chunk make_chunk(const SerdChunk& chunk) { + return Chunk((const char*)chunk.buf, chunk.len); + } + + SerdNode _node; + SerdURI _uri; +}; + +inline bool operator==(const URI& lhs, const URI& rhs) +{ + return lhs.string() == rhs.string(); +} + +inline bool operator==(const URI& lhs, const std::string& rhs) +{ + return lhs.string() == rhs; +} + +inline bool operator==(const URI& lhs, const char* rhs) +{ + return lhs.string() == rhs; +} + +inline bool operator==(const URI& lhs, const Sord::Node& rhs) +{ + return rhs.type() == Sord::Node::URI && lhs.string() == rhs.to_string(); +} + +inline bool operator==(const Sord::Node& lhs, const URI& rhs) +{ + return rhs == lhs; +} + +inline bool operator!=(const URI& lhs, const URI& rhs) +{ + return lhs.string() != rhs.string(); +} + +inline bool operator!=(const URI& lhs, const std::string& rhs) +{ + return lhs.string() != rhs; +} + +inline bool operator!=(const URI& lhs, const char* rhs) +{ + return lhs.string() != rhs; +} + +inline bool operator!=(const URI& lhs, const Sord::Node& rhs) +{ + return !(lhs == rhs); +} + +inline bool operator!=(const Sord::Node& lhs, const URI& rhs) +{ + return !(lhs == rhs); +} + +inline bool operator<(const URI& lhs, const URI& rhs) +{ + return lhs.string() < rhs.string(); +} + +template <typename Char, typename Traits> +inline std::basic_ostream<Char, Traits>& +operator<<(std::basic_ostream<Char, Traits>& os, const URI& uri) +{ + return os << uri.string(); +} + +} // namespace Ingen + +#endif // INGEN_URI_HPP diff --git a/ingen/URIMap.hpp b/ingen/URIMap.hpp new file mode 100644 index 00000000..90c5f0ac --- /dev/null +++ b/ingen/URIMap.hpp @@ -0,0 +1,100 @@ +/* + 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_URIMAP_HPP +#define INGEN_URIMAP_HPP + +#include <cstdint> +#include <mutex> +#include <string> +#include <unordered_map> +#include <utility> +#include <vector> + +#include "ingen/LV2Features.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" +#include "raul/Noncopyable.hpp" + +namespace Ingen { + +class Log; +class Node; +class World; + +/** URI to integer map and implementation of LV2 URID extension. + * @ingroup IngenShared + */ +class INGEN_API URIMap : public Raul::Noncopyable { +public: + URIMap(Log& log, LV2_URID_Map* map, LV2_URID_Unmap* unmap); + + uint32_t map_uri(const char* uri); + uint32_t map_uri(const std::string& uri) { return map_uri(uri.c_str()); } + const char* unmap_uri(uint32_t urid) const; + + class Feature : public LV2Features::Feature { + public: + Feature(const char* URI, void* data) { + _feature.URI = URI; + _feature.data = data; + } + + const char* uri() const override { return _feature.URI; } + + SPtr<LV2_Feature> feature(World*, Node*) override { + return SPtr<LV2_Feature>(&_feature, NullDeleter<LV2_Feature>); + } + + private: + LV2_Feature _feature; + }; + + struct URIDMapFeature : public Feature { + URIDMapFeature(URIMap* map, LV2_URID_Map* impl, Log& log); + LV2_URID map(const char* uri); + static LV2_URID default_map(LV2_URID_Map_Handle h, const char* c_uri); + LV2_URID_Map urid_map; + Log& log; + }; + + struct URIDUnmapFeature : public Feature { + URIDUnmapFeature(URIMap* map, LV2_URID_Unmap* impl); + const char* unmap(const LV2_URID urid); + static const char* default_unmap(LV2_URID_Map_Handle h, LV2_URID urid); + LV2_URID_Unmap urid_unmap; + }; + + SPtr<URIDMapFeature> urid_map_feature() { return _urid_map_feature; } + SPtr<URIDUnmapFeature> urid_unmap_feature() { return _urid_unmap_feature; } + +private: + friend struct URIDMapFeature; + friend struct URIDUnMapFeature; + + SPtr<URIDMapFeature> _urid_map_feature; + SPtr<URIDUnmapFeature> _urid_unmap_feature; + + std::mutex _mutex; + std::unordered_map<std::string, LV2_URID> _map; + std::vector<std::string> _unmap; +}; + +} // namespace Ingen + +#endif // INGEN_URIMAP_HPP diff --git a/ingen/URIs.hpp b/ingen/URIs.hpp new file mode 100644 index 00000000..71c14d14 --- /dev/null +++ b/ingen/URIs.hpp @@ -0,0 +1,234 @@ +/* + 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_URIS_HPP +#define INGEN_URIS_HPP + +#include "ingen/Atom.hpp" +#include "ingen/URI.hpp" +#include "ingen/ingen.h" +#include "lilv/lilv.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "raul/Noncopyable.hpp" + +namespace Ingen { + +class Forge; +class URIMap; + +/** Frequently used interned URIs. + * + * This class initially maps all the special URIs used throughout the code + * using the URIMap so they can be used quickly with the performance of + * integers, but still be dynamic. + * + * @ingroup ingen + */ +class INGEN_API URIs : public Raul::Noncopyable { +public: + URIs(Ingen::Forge& forge, URIMap* map, LilvWorld* lworld); + + struct Quark : public URI { + Quark(Ingen::Forge& forge, + URIMap* map, + LilvWorld* lworld, + const char* str); + + Quark(const Quark& copy); + + ~Quark(); + + operator LV2_URID() const { return urid.get<LV2_URID>(); } + explicit operator Atom() const { return urid; } + operator const LilvNode*() const { return lnode; } + + Atom urid; + Atom uri; + LilvNode* lnode; + }; + + Ingen::Forge& forge; + + const Quark atom_AtomPort; + const Quark atom_Bool; + const Quark atom_Chunk; + const Quark atom_Float; + const Quark atom_Int; + const Quark atom_Object; + const Quark atom_Path; + const Quark atom_Sequence; + const Quark atom_Sound; + const Quark atom_String; + const Quark atom_URI; + const Quark atom_URID; + const Quark atom_bufferType; + const Quark atom_eventTransfer; + const Quark atom_supports; + const Quark bufsz_maxBlockLength; + const Quark bufsz_minBlockLength; + const Quark bufsz_sequenceSize; + const Quark doap_name; + const Quark ingen_Arc; + const Quark ingen_Block; + const Quark ingen_BundleEnd; + const Quark ingen_BundleStart; + const Quark ingen_Graph; + const Quark ingen_GraphPrototype; + const Quark ingen_Internal; + const Quark ingen_Redo; + const Quark ingen_Undo; + const Quark ingen_activity; + const Quark ingen_arc; + const Quark ingen_block; + const Quark ingen_broadcast; + const Quark ingen_canvasX; + const Quark ingen_canvasY; + const Quark ingen_enabled; + const Quark ingen_externalContext; + const Quark ingen_file; + const Quark ingen_head; + const Quark ingen_incidentTo; + const Quark ingen_internalContext; + const Quark ingen_loadedBundle; + const Quark ingen_maxRunLoad; + const Quark ingen_meanRunLoad; + const Quark ingen_minRunLoad; + const Quark ingen_numThreads; + const Quark ingen_polyphonic; + const Quark ingen_polyphony; + const Quark ingen_prototype; + const Quark ingen_sprungLayout; + const Quark ingen_tail; + const Quark ingen_uiEmbedded; + const Quark ingen_value; + const Quark log_Error; + const Quark log_Note; + const Quark log_Trace; + const Quark log_Warning; + const Quark lv2_AudioPort; + const Quark lv2_CVPort; + const Quark lv2_ControlPort; + const Quark lv2_InputPort; + const Quark lv2_OutputPort; + const Quark lv2_Plugin; + const Quark lv2_appliesTo; + const Quark lv2_binary; + const Quark lv2_connectionOptional; + const Quark lv2_control; + const Quark lv2_default; + const Quark lv2_designation; + const Quark lv2_enumeration; + const Quark lv2_extensionData; + const Quark lv2_index; + const Quark lv2_integer; + const Quark lv2_maximum; + const Quark lv2_microVersion; + const Quark lv2_minimum; + const Quark lv2_minorVersion; + const Quark lv2_name; + const Quark lv2_port; + const Quark lv2_portProperty; + const Quark lv2_prototype; + const Quark lv2_sampleRate; + const Quark lv2_scalePoint; + const Quark lv2_symbol; + const Quark lv2_toggled; + const Quark midi_Bender; + const Quark midi_ChannelPressure; + const Quark midi_Controller; + const Quark midi_MidiEvent; + const Quark midi_NoteOn; + const Quark midi_binding; + const Quark midi_controllerNumber; + const Quark midi_noteNumber; + const Quark morph_AutoMorphPort; + const Quark morph_MorphPort; + const Quark morph_currentType; + const Quark morph_supportsType; + const Quark opt_interface; + const Quark param_sampleRate; + const Quark patch_Copy; + const Quark patch_Delete; + const Quark patch_Get; + const Quark patch_Message; + const Quark patch_Move; + const Quark patch_Patch; + const Quark patch_Put; + const Quark patch_Response; + const Quark patch_Set; + const Quark patch_add; + const Quark patch_body; + const Quark patch_context; + const Quark patch_destination; + const Quark patch_property; + const Quark patch_remove; + const Quark patch_sequenceNumber; + const Quark patch_subject; + const Quark patch_value; + const Quark patch_wildcard; + const Quark pprops_logarithmic; + const Quark pset_Preset; + const Quark pset_preset; + const Quark rdf_type; + const Quark rdfs_Class; + const Quark rdfs_label; + const Quark rdfs_seeAlso; + const Quark rsz_minimumSize; + const Quark state_loadDefaultState; + const Quark state_state; + const Quark time_Position; + const Quark time_bar; + const Quark time_barBeat; + const Quark time_beatUnit; + const Quark time_beatsPerBar; + const Quark time_beatsPerMinute; + const Quark time_frame; + const Quark time_speed; + const Quark work_schedule; +}; + +inline bool +operator==(const URIs::Quark& lhs, const Atom& rhs) +{ + if (rhs.type() == lhs.urid.type()) { + return rhs == lhs.urid; + } else if (rhs.type() == lhs.uri.type()) { + return rhs == lhs.uri; + } + return false; +} + +inline bool +operator==(const Atom& lhs, const URIs::Quark& rhs) +{ + return rhs == lhs; +} + +inline bool +operator!=(const Atom& lhs, const URIs::Quark& rhs) +{ + return !(lhs == rhs); +} + +inline bool +operator!=(const URIs::Quark& lhs, const Atom& rhs) +{ + return !(lhs == rhs); +} + +} // namespace Ingen + +#endif // INGEN_URIS_HPP diff --git a/ingen/World.hpp b/ingen/World.hpp new file mode 100644 index 00000000..13bd7001 --- /dev/null +++ b/ingen/World.hpp @@ -0,0 +1,150 @@ +/* + 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_WORLD_HPP +#define INGEN_WORLD_HPP + +#include <mutex> +#include <string> + +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "raul/Noncopyable.hpp" + +typedef struct LilvWorldImpl LilvWorld; + +namespace Sord { class World; } + +namespace Ingen { + +class Configuration; +class EngineBase; +class Forge; +class Interface; +class LV2Features; +class Log; +class Parser; +class Serialiser; +class Store; +class URI; +class URIMap; +class URIs; + +/** The "world" all Ingen modules share. + * + * This is the root to which all components of Ingen are connected. It + * contains all necessary shared data (including the world for libraries like + * Sord and Lilv) and holds references to components. + * + * Some functionality in Ingen is implemented in dynamically loaded modules, + * which are loaded using this interface. When loaded, those modules add + * facilities to the World which can then be used throughout the code. + * + * The world is used in any process which uses the Ingen as a library, both + * client and server (e.g. the world may not actually contain an Engine, since + * it maybe running in another process or even on a different machine). + * + * @ingroup IngenShared + */ +class INGEN_API World : public Raul::Noncopyable { +public: + /** Construct a new Ingen world. + * @param map LV2 URID map implementation, or NULL to use internal. + * @param unmap LV2 URID unmap implementation, or NULL to use internal. + * @param log LV2 log implementation, or NULL to use internal. + */ + World(LV2_URID_Map* map, LV2_URID_Unmap* unmap, LV2_Log_Log* log); + + virtual ~World(); + + /** Load configuration from files and command line. + * @param argc Argument count (as in C main()) + * @param argv Argument vector (as in C main()) + */ + virtual void load_configuration(int& argc, char**& argv); + + /** Load an Ingen module by name (e.g. "server", "gui", etc.) + * @return True on success. + */ + virtual bool load_module(const char* name); + + /** Run a loaded module (modules that "run" only, e.g. gui). + * @return True on success. + */ + virtual bool run_module(const char* name); + + /** A function to create a new remote Interface. */ + typedef SPtr<Interface> (*InterfaceFactory)(World* world, + const URI& engine_uri, + SPtr<Interface> respondee); + + /** Register an InterfaceFactory (for module implementations). */ + virtual void add_interface_factory(const std::string& scheme, + InterfaceFactory factory); + + /** Return a new Interface to control a server. + * @param engine_uri The URI of the possibly remote server to control. + * @param respondee The Interface that will receive responses to commands + * and broadcasts, if applicable. + */ + virtual SPtr<Interface> new_interface(const URI& engine_uri, + SPtr<Interface> respondee); + + /** Run a script. */ + virtual bool run(const std::string& mime_type, + const std::string& filename); + + virtual void set_engine(SPtr<EngineBase> e); + virtual void set_interface(SPtr<Interface> i); + virtual void set_store(SPtr<Store> s); + + virtual SPtr<EngineBase> engine(); + virtual SPtr<Interface> interface(); + virtual SPtr<Parser> parser(); + virtual SPtr<Serialiser> serialiser(); + virtual SPtr<Store> store(); + + virtual int& argc(); + virtual char**& argv(); + virtual Configuration& conf(); + + /** Lock for rdf_world() or lilv_world(). */ + virtual std::mutex& rdf_mutex(); + + virtual Sord::World* rdf_world(); + virtual LilvWorld* lilv_world(); + + virtual LV2Features& lv2_features(); + virtual Ingen::Forge& forge(); + virtual URIMap& uri_map(); + virtual URIs& uris(); + + virtual void set_jack_uuid(const std::string& uuid); + virtual std::string jack_uuid(); + + virtual Log& log(); + +private: + class Impl; + + Impl* _impl; +}; + +} // namespace Ingen + +#endif // INGEN_WORLD_HPP diff --git a/ingen/client/ArcModel.hpp b/ingen/client/ArcModel.hpp new file mode 100644 index 00000000..8b129a00 --- /dev/null +++ b/ingen/client/ArcModel.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_CLIENT_ARCMODEL_HPP +#define INGEN_CLIENT_ARCMODEL_HPP + +#include <cassert> + +#include "ingen/types.hpp" +#include "raul/Path.hpp" + +#include "ingen/Arc.hpp" +#include "ingen/client/PortModel.hpp" +#include "ingen/ingen.h" + +namespace Ingen { +namespace Client { + +class ClientStore; + +/** Class to represent a port->port connections in the engine. + * + * @ingroup IngenClient + */ +class INGEN_API ArcModel : public Arc +{ +public: + SPtr<PortModel> tail() const { return _tail; } + SPtr<PortModel> head() const { return _head; } + + const Raul::Path& tail_path() const { return _tail->path(); } + const Raul::Path& head_path() const { return _head->path(); } + +private: + friend class ClientStore; + + ArcModel(SPtr<PortModel> tail, SPtr<PortModel> head) + : _tail(std::move(tail)) + , _head(std::move(head)) + { + assert(_tail); + assert(_head); + assert(_tail->parent()); + assert(_head->parent()); + assert(_tail->path() != _head->path()); + } + + const SPtr<PortModel> _tail; + const SPtr<PortModel> _head; +}; + +} // namespace Client +} // namespace Ingen + +#endif // INGEN_CLIENT_ARCMODEL_HPP diff --git a/ingen/client/BlockModel.hpp b/ingen/client/BlockModel.hpp new file mode 100644 index 00000000..38e8987e --- /dev/null +++ b/ingen/client/BlockModel.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_CLIENT_BLOCKMODEL_HPP +#define INGEN_CLIENT_BLOCKMODEL_HPP + +#include <cstdlib> +#include <string> +#include <vector> + +#include "ingen/Node.hpp" +#include "ingen/client/ObjectModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/client/PortModel.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" + +namespace Raul { class Path; } + +namespace Ingen { + +class URIs; + +namespace Client { + +class PluginModel; +class ClientStore; + +/** Block model class, used by the client to store engine's state. + * + * @ingroup IngenClient + */ +class INGEN_API BlockModel : public ObjectModel +{ +public: + BlockModel(const BlockModel& copy); + virtual ~BlockModel(); + + GraphType graph_type() const { return Node::GraphType::BLOCK; } + + typedef std::vector< SPtr<const PortModel> > Ports; + + SPtr<const PortModel> get_port(const Raul::Symbol& symbol) const; + SPtr<const PortModel> get_port(uint32_t index) const; + + Node* port(uint32_t index) const; + + const URI& plugin_uri() const { return _plugin_uri; } + const Resource* plugin() const { return _plugin.get(); } + Resource* plugin() { return _plugin.get(); } + SPtr<PluginModel> plugin_model() const { return _plugin; } + uint32_t num_ports() const { return _ports.size(); } + const Ports& ports() const { return _ports; } + + void default_port_value_range(SPtr<const PortModel> port, + float& min, + float& max, + uint32_t srate = 1) const; + + void port_value_range(SPtr<const PortModel> port, + float& min, + float& max, + uint32_t srate = 1) const; + + std::string label() const; + std::string port_label(SPtr<const PortModel> port) const; + + // Signals + INGEN_SIGNAL(new_port, void, SPtr<const PortModel>); + INGEN_SIGNAL(removed_port, void, SPtr<const PortModel>); + +protected: + friend class ClientStore; + + BlockModel(URIs& uris, + const URI& plugin_uri, + const Raul::Path& path); + BlockModel(URIs& uris, + SPtr<PluginModel> plugin, + const Raul::Path& path); + explicit BlockModel(const Raul::Path& path); + + void add_child(SPtr<ObjectModel> c); + bool remove_child(SPtr<ObjectModel> c); + void add_port(SPtr<PortModel> pm); + void remove_port(SPtr<PortModel> port); + void remove_port(const Raul::Path& port_path); + void set(SPtr<ObjectModel> model); + + virtual void clear(); + + Ports _ports; ///< Vector of ports + URI _plugin_uri; ///< Plugin URI (if PluginModel is unknown) + SPtr<PluginModel> _plugin; ///< The plugin this block is an instance of + +private: + mutable uint32_t _num_values; ///< Size of _min_values and _max_values + mutable float* _min_values; ///< Port min values (cached for LV2) + mutable float* _max_values; ///< Port max values (cached for LV2) +}; + +} // namespace Client +} // namespace Ingen + +#endif // INGEN_CLIENT_BLOCKMODEL_HPP diff --git a/ingen/client/ClientStore.hpp b/ingen/client/ClientStore.hpp new file mode 100644 index 00000000..797052ef --- /dev/null +++ b/ingen/client/ClientStore.hpp @@ -0,0 +1,127 @@ +/* + 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_CLIENT_CLIENTSTORE_HPP +#define INGEN_CLIENT_CLIENTSTORE_HPP + +#include <cassert> +#include <list> +#include <string> + +#include "ingen/Interface.hpp" +#include "ingen/Store.hpp" +#include "ingen/client/signal.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "raul/Path.hpp" + +namespace Raul { class Atom; } + +namespace Ingen { + +class Log; +class Node; +class URIs; + +namespace Client { + +class BlockModel; +class GraphModel; +class ObjectModel; +class PluginModel; +class PortModel; +class SigClientInterface; + +/** Automatically manages models of objects in the engine. + * + * @ingroup IngenClient + */ +class INGEN_API ClientStore : public Store + , public Interface + , public INGEN_TRACKABLE { +public: + ClientStore( + URIs& uris, + Log& log, + SPtr<SigClientInterface> emitter = SPtr<SigClientInterface>()); + + URI uri() const override { return URI("ingen:/clients/store"); } + + SPtr<const ObjectModel> object(const Raul::Path& path) const; + SPtr<const PluginModel> plugin(const URI& uri) const; + SPtr<const Resource> resource(const URI& uri) const; + + void clear(); + + typedef std::map< const URI, SPtr<PluginModel> > Plugins; + SPtr<const Plugins> plugins() const { return _plugins; } + SPtr<Plugins> plugins() { return _plugins; } + void set_plugins(SPtr<Plugins> p) { _plugins = p; } + + URIs& uris() { return _uris; } + + void message(const Message& msg) override; + + 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&) {} + + INGEN_SIGNAL(new_object, void, SPtr<ObjectModel>); + INGEN_SIGNAL(new_plugin, void, SPtr<PluginModel>); + INGEN_SIGNAL(plugin_deleted, void, URI); + +private: + SPtr<ObjectModel> _object(const Raul::Path& path); + SPtr<PluginModel> _plugin(const URI& uri); + SPtr<PluginModel> _plugin(const Atom& uri); + SPtr<Resource> _resource(const URI& uri); + + void add_object(SPtr<ObjectModel> object); + SPtr<ObjectModel> remove_object(const Raul::Path& path); + + void add_plugin(SPtr<PluginModel> pm); + + SPtr<GraphModel> connection_graph(const Raul::Path& tail_path, + const Raul::Path& head_path); + + // Slots for SigClientInterface signals + bool attempt_connection(const Raul::Path& tail_path, + const Raul::Path& head_path); + + URIs& _uris; + Log& _log; + SPtr<SigClientInterface> _emitter; + + SPtr<Plugins> _plugins; ///< Map, keyed by plugin URI +}; + +} // namespace Client +} // namespace Ingen + +#endif // INGEN_CLIENT_CLIENTSTORE_HPP diff --git a/ingen/client/GraphModel.hpp b/ingen/client/GraphModel.hpp new file mode 100644 index 00000000..ef072d87 --- /dev/null +++ b/ingen/client/GraphModel.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_CLIENT_GRAPHMODEL_HPP +#define INGEN_CLIENT_GRAPHMODEL_HPP + +#include "ingen/client/BlockModel.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" + +namespace Ingen { +namespace Client { + +class ArcModel; +class ClientStore; + +/** Client's model of a graph. + * + * @ingroup IngenClient + */ +class INGEN_API GraphModel : public BlockModel +{ +public: + /* WARNING: Copy constructor creates a shallow copy WRT connections */ + + GraphType graph_type() const { return Node::GraphType::GRAPH; } + + SPtr<ArcModel> get_arc(const Ingen::Node* tail, + const Ingen::Node* head); + + bool enabled() const; + bool polyphonic() const; + uint32_t internal_poly() const; + + // Signals + INGEN_SIGNAL(new_block, void, SPtr<BlockModel>); + INGEN_SIGNAL(removed_block, void, SPtr<BlockModel>); + INGEN_SIGNAL(new_arc, void, SPtr<ArcModel>); + INGEN_SIGNAL(removed_arc, void, SPtr<ArcModel>); + +private: + friend class ClientStore; + + GraphModel(URIs& uris, const Raul::Path& graph_path) + : BlockModel(uris, uris.ingen_Graph, graph_path) + {} + + void clear(); + void add_child(SPtr<ObjectModel> c); + bool remove_child(SPtr<ObjectModel> o); + void remove_arcs_on(SPtr<PortModel> p); + + void add_arc(SPtr<ArcModel> arc); + void remove_arc(const Ingen::Node* tail, + const Ingen::Node* head); +}; + +} // namespace Client +} // namespace Ingen + +#endif // INGEN_CLIENT_GRAPHMODEL_HPP diff --git a/ingen/client/ObjectModel.hpp b/ingen/client/ObjectModel.hpp new file mode 100644 index 00000000..a5a68f1e --- /dev/null +++ b/ingen/client/ObjectModel.hpp @@ -0,0 +1,103 @@ +/* + 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/>. +*/ + +/** + @defgroup IngenClient Client-Side Models and Utilities +*/ + +#ifndef INGEN_CLIENT_OBJECTMODEL_HPP +#define INGEN_CLIENT_OBJECTMODEL_HPP + +#include <algorithm> +#include <cassert> +#include <cstdlib> + +#include "ingen/types.hpp" +#include "raul/Path.hpp" + +#include "ingen/Node.hpp" +#include "ingen/Resource.hpp" +#include "ingen/client/signal.hpp" +#include "ingen/ingen.h" + +namespace Ingen { + +class URIs; + +namespace Client { + +class ClientStore; + +/** Base class for all Node models (BlockModel, GraphModel, PortModel). + * + * There are no non-const public methods intentionally, models are not allowed + * to be manipulated directly by anything (but the Store) because of the + * asynchronous nature of engine control. To change something, use the + * controller (which the model probably shouldn't have a reference to but oh + * well, it reduces Collection Hell) and wait for the result (as a signal + * from this Model). + * + * @ingroup IngenClient + */ +class INGEN_API ObjectModel : public Node +{ +public: + bool is_a(const URIs::Quark& type) const; + + const Atom& get_property(const URI& key) const; + + void on_property(const URI& uri, const Atom& value); + void on_property_removed(const URI& uri, const Atom& value); + + const Raul::Path& path() const { return _path; } + const Raul::Symbol& symbol() const { return _symbol; } + SPtr<ObjectModel> parent() const { return _parent; } + bool polyphonic() const; + + Node* graph_parent() const { return _parent.get(); } + + // Signals + INGEN_SIGNAL(new_child, void, SPtr<ObjectModel>); + INGEN_SIGNAL(removed_child, void, SPtr<ObjectModel>); + INGEN_SIGNAL(property, void, const URI&, const Atom&); + INGEN_SIGNAL(property_removed, void, const URI&, const Atom&); + INGEN_SIGNAL(destroyed, void); + INGEN_SIGNAL(moved, void); + +protected: + friend class ClientStore; + + ObjectModel(URIs& uris, const Raul::Path& path); + ObjectModel(const ObjectModel& copy); + + virtual void set_path(const Raul::Path& p); + virtual void set_parent(SPtr<ObjectModel> p); + virtual void add_child(SPtr<ObjectModel> c) {} + virtual bool remove_child(SPtr<ObjectModel> c) { return true; } + + virtual void set(SPtr<ObjectModel> o); + + SPtr<ObjectModel> _parent; + +private: + Raul::Path _path; + Raul::Symbol _symbol; +}; + +} // namespace Client +} // namespace Ingen + +#endif // INGEN_CLIENT_OBJECTMODEL_HPP diff --git a/ingen/client/PluginModel.hpp b/ingen/client/PluginModel.hpp new file mode 100644 index 00000000..61de0f1a --- /dev/null +++ b/ingen/client/PluginModel.hpp @@ -0,0 +1,128 @@ +/* + 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_CLIENT_PLUGINMODEL_HPP +#define INGEN_CLIENT_PLUGINMODEL_HPP + +#include <list> +#include <map> +#include <string> +#include <utility> + +#include "ingen/Forge.hpp" +#include "ingen/Resource.hpp" +#include "ingen/World.hpp" +#include "ingen/client/signal.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "lilv/lilv.h" +#include "raul/Symbol.hpp" +#include "sord/sordmm.hpp" + +namespace Ingen { + +class URIs; + +namespace Client { + +class GraphModel; +class BlockModel; +class PluginUI; + +/** Model for a plugin available for loading. + * + * @ingroup IngenClient + */ +class INGEN_API PluginModel : public Ingen::Resource +{ +public: + PluginModel(URIs& uris, + const URI& uri, + const Atom& type, + const Ingen::Properties& properties); + + const Atom& type() const { return _type; } + + const URI type_uri() const + { + return URI(_type.is_valid() ? _uris.forge.str(_type, false) + : "http://www.w3.org/2002/07/owl#Nothing"); + } + + virtual const Atom& get_property(const URI& key) const; + + Raul::Symbol default_block_symbol() const; + std::string human_name() const; + std::string port_human_name(uint32_t i) const; + + typedef std::map<float, std::string> ScalePoints; + ScalePoints port_scale_points(uint32_t i) const; + + typedef std::map<URI, std::string> Presets; + const Presets& presets() const { return _presets; } + + static LilvWorld* lilv_world() { return _lilv_world; } + const LilvPlugin* lilv_plugin() const { return _lilv_plugin; } + + const LilvPort* lilv_port(uint32_t index) const; + + static void set_lilv_world(LilvWorld* world); + + bool has_ui() const; + + SPtr<PluginUI> ui(Ingen::World* world, + SPtr<const BlockModel> block) const; + + std::string documentation(bool html) const; + std::string port_documentation(uint32_t index, bool html) const; + + static void set_rdf_world(Sord::World& world) { + _rdf_world = &world; + } + + static Sord::World* rdf_world() { return _rdf_world; } + + // Signals + INGEN_SIGNAL(changed, void); + INGEN_SIGNAL(property, void, const URI&, const Atom&); + INGEN_SIGNAL(preset, void, const URI&, const std::string&); + + bool fetched() const { return _fetched; } + void set_fetched(bool f) { _fetched = f; } + +protected: + friend class ClientStore; + void set(SPtr<PluginModel> p); + + void add_preset(const URI& uri, const std::string& label); + +private: + std::string get_documentation(const LilvNode* subject, bool html) const; + + static Sord::World* _rdf_world; + static LilvWorld* _lilv_world; + static const LilvPlugins* _lilv_plugins; + + Atom _type; + const LilvPlugin* _lilv_plugin; + Presets _presets; + bool _fetched; +}; + +} // namespace Client +} // namespace Ingen + +#endif // INGEN_CLIENT_PLUGINMODEL_HPP diff --git a/ingen/client/PluginUI.hpp b/ingen/client/PluginUI.hpp new file mode 100644 index 00000000..a98df61d --- /dev/null +++ b/ingen/client/PluginUI.hpp @@ -0,0 +1,111 @@ +/* + 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_CLIENT_PLUGINUI_HPP +#define INGEN_CLIENT_PLUGINUI_HPP + +#include <set> + +#include "ingen/LV2Features.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "lilv/lilv.h" +#include "suil/suil.h" + +namespace Ingen { + +class Interface; +class World; + +namespace Client { + +class BlockModel; + +/** Custom user interface for a plugin. + * + * @ingroup IngenClient + */ +class INGEN_API PluginUI { +public: + ~PluginUI(); + + /** Create a UI for the given block and plugin. + * + * This does not actually instantiate the UI itself, so signals can be + * connected first. The caller should connect to signal_property_changed, + * then call instantiate(). + */ + static SPtr<PluginUI> create(Ingen::World* world, + SPtr<const BlockModel> block, + const LilvPlugin* plugin); + + /** Instantiate the UI. + * + * If true is returned, instantiation was successfull and the widget can be + * obtained with get_widget(). Otherwise, instantiation failed, so there is + * no widget and the UI can not be used. + */ + bool instantiate(); + bool instantiated () { return _instance != nullptr; } + + SuilWidget get_widget(); + + void port_event(uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void* buffer); + + bool is_resizable() const; + + /** Signal emitted when the UI sets a property. + * + * The application must connect to this signal to communicate with the + * engine and/or update itself as necessary. + */ + INGEN_SIGNAL(property_changed, void, + const URI&, // Subject + const URI&, // Predicate + const Atom&, // Object + Resource::Graph); // Context + + Ingen::World* world() const { return _world; } + SPtr<const BlockModel> block() const { return _block; } + +private: + PluginUI(Ingen::World* world, + SPtr<const BlockModel> block, + LilvUIs* uis, + const LilvUI* ui, + const LilvNode* ui_type); + + Ingen::World* _world; + SPtr<const BlockModel> _block; + SuilInstance* _instance; + LilvUIs* _uis; + const LilvUI* _ui; + LilvNode* _ui_node; + LilvNode* _ui_type; + std::set<uint32_t> _subscribed_ports; + + static SuilHost* ui_host; + + SPtr<LV2Features::FeatureArray> _features; +}; + +} // namespace Client +} // namespace Ingen + +#endif // INGEN_CLIENT_PLUGINUI_HPP diff --git a/ingen/client/PortModel.hpp b/ingen/client/PortModel.hpp new file mode 100644 index 00000000..9ad37378 --- /dev/null +++ b/ingen/client/PortModel.hpp @@ -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/>. +*/ + +#ifndef INGEN_CLIENT_PORTMODEL_HPP +#define INGEN_CLIENT_PORTMODEL_HPP + +#include <cstdlib> +#include <string> + +#include "ingen/client/ObjectModel.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/ext/port-props/port-props.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +namespace Raul { class Path; } + +namespace Ingen { +namespace Client { + +/** Model of a port. + * + * @ingroup IngenClient + */ +class INGEN_API PortModel : public ObjectModel +{ +public: + enum class Direction { INPUT, OUTPUT }; + + GraphType graph_type() const { return Node::GraphType::PORT; } + + bool supports(const URIs::Quark& value_type) const; + + inline uint32_t index() const { return _index; } + inline const Atom& value() const { return get_property(_uris.ingen_value); } + inline bool is_input() const { return (_direction == Direction::INPUT); } + inline bool is_output() const { return (_direction == Direction::OUTPUT); } + + bool port_property(const URIs::Quark& uri) const; + + bool is_logarithmic() const { return port_property(_uris.pprops_logarithmic); } + bool is_enumeration() const { return port_property(_uris.lv2_enumeration); } + bool is_integer() const { return port_property(_uris.lv2_integer); } + bool is_toggle() const { return port_property(_uris.lv2_toggled); } + bool is_numeric() const { + return ObjectModel::is_a(_uris.lv2_ControlPort) + || ObjectModel::is_a(_uris.lv2_CVPort); + } + bool is_uri() const; + + inline bool operator==(const PortModel& pm) const { return (path() == pm.path()); } + + void on_property(const URI& uri, const Atom& value); + + // Signals + INGEN_SIGNAL(value_changed, void, const Atom&); + INGEN_SIGNAL(voice_changed, void, uint32_t, const Atom&); + INGEN_SIGNAL(activity, void, const Atom&); + +private: + friend class ClientStore; + + PortModel(URIs& uris, + const Raul::Path& path, + uint32_t index, + Direction dir) + : ObjectModel(uris, path) + , _index(index) + , _direction(dir) + {} + + void add_child(SPtr<ObjectModel> c) { throw; } + bool remove_child(SPtr<ObjectModel> c) { throw; } + + void set(SPtr<ObjectModel> model); + + uint32_t _index; + Direction _direction; +}; + +} // namespace Client +} // namespace Ingen + +#endif // INGEN_CLIENT_PORTMODEL_HPP diff --git a/ingen/client/SigClientInterface.hpp b/ingen/client/SigClientInterface.hpp new file mode 100644 index 00000000..8ac8dca4 --- /dev/null +++ b/ingen/client/SigClientInterface.hpp @@ -0,0 +1,64 @@ +/* + 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_CLIENT_SIGCLIENTINTERFACE_HPP +#define INGEN_CLIENT_SIGCLIENTINTERFACE_HPP + +#include <cstdint> +#include <string> + +#include "raul/Path.hpp" + +#include "ingen/Interface.hpp" +#include "ingen/client/signal.hpp" +#include "ingen/ingen.h" + +namespace Ingen { +namespace Client { + +/** A LibSigC++ signal emitting interface for clients to use. + * + * This simply emits a signal for every event that comes from the engine. + * For a higher level model based view of the engine, use ClientStore. + * + * The signals here match the calls to ClientInterface exactly. See the + * documentation for ClientInterface for meanings of signal parameters. + * + * @ingroup IngenClient + */ +class INGEN_API SigClientInterface : public Ingen::Interface, + public INGEN_TRACKABLE +{ +public: + SigClientInterface() {} + + URI uri() const override { return URI("ingen:/clients/sig"); } + + INGEN_SIGNAL(message, void, Message) + + /** Fire pending signals. Only does anything on derived classes (that may queue) */ + virtual bool emit_signals() { return false; } + +protected: + void message(const Message& msg) override { + _signal_message(msg); + } +}; + +} // namespace Client +} // namespace Ingen + +#endif diff --git a/ingen/client/SocketClient.hpp b/ingen/client/SocketClient.hpp new file mode 100644 index 00000000..8236200b --- /dev/null +++ b/ingen/client/SocketClient.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2012-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_CLIENT_SOCKET_CLIENT_HPP +#define INGEN_CLIENT_SOCKET_CLIENT_HPP + +#include "ingen/SocketReader.hpp" +#include "ingen/SocketWriter.hpp" +#include "ingen/ingen.h" +#include "raul/Socket.hpp" + +namespace Ingen { +namespace Client { + +/** The client side of an Ingen socket connection. */ +class INGEN_API SocketClient : public SocketWriter +{ +public: + SocketClient(World& world, + const URI& uri, + SPtr<Raul::Socket> sock, + SPtr<Interface> respondee) + : SocketWriter(world.uri_map(), world.uris(), uri, sock) + , _respondee(respondee) + , _reader(world, *respondee.get(), sock) + {} + + SPtr<Interface> respondee() const override { + return _respondee; + } + + void set_respondee(SPtr<Interface> respondee) override { + _respondee = respondee; + } + + static SPtr<Ingen::Interface> + new_socket_interface(Ingen::World* world, + const URI& uri, + SPtr<Ingen::Interface> respondee) + { + const Raul::Socket::Type type = (uri.scheme() == "unix" + ? Raul::Socket::Type::UNIX + : Raul::Socket::Type::TCP); + + SPtr<Raul::Socket> sock(new Raul::Socket(type)); + if (!sock->connect(uri)) { + world->log().error(fmt("Failed to connect <%1%> (%2%)\n") + % sock->uri() % strerror(errno)); + return SPtr<Interface>(); + } + return SPtr<Interface>(new SocketClient(*world, uri, sock, respondee)); + } + + static void register_factories(World* world) { + world->add_interface_factory("unix", &new_socket_interface); + world->add_interface_factory("tcp", &new_socket_interface); + } + +private: + SPtr<Interface> _respondee; + SocketReader _reader; +}; + +} // namespace Client +} // namespace Ingen + +#endif // INGEN_CLIENT_SOCKET_CLIENT_HPP diff --git a/ingen/client/signal.hpp b/ingen/client/signal.hpp new file mode 100644 index 00000000..ba5b017b --- /dev/null +++ b/ingen/client/signal.hpp @@ -0,0 +1,31 @@ +/* + 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_CLIENT_SIGNAL_HPP +#define INGEN_CLIENT_SIGNAL_HPP + +#include <sigc++/sigc++.h> + +#define INGEN_SIGNAL(name, ...) \ +protected: \ +sigc::signal<__VA_ARGS__> _signal_##name; \ +public: \ +sigc::signal<__VA_ARGS__> signal_##name() const { return _signal_##name; } \ +sigc::signal<__VA_ARGS__>& signal_##name() { return _signal_##name; } + +#define INGEN_TRACKABLE sigc::trackable + +#endif // INGEN_CLIENT_SIGNAL_HPP diff --git a/ingen/filesystem.hpp b/ingen/filesystem.hpp new file mode 100644 index 00000000..5c7d7568 --- /dev/null +++ b/ingen/filesystem.hpp @@ -0,0 +1,86 @@ +/* + 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_FILESYSTEM_HPP +#define INGEN_FILESYSTEM_HPP + +#define _BSD_SOURCE 1 +#define _DEFAULT_SOURCE 1 + +#include <cerrno> +#include <climits> +#include <cstdlib> +#include <memory> +#include <vector> + +#include <sys/stat.h> +#include <sys/types.h> + +#ifdef _WIN32 +# include <windows.h> +# include <io.h> +# define F_OK 0 +# define mkdir(path, flags) _mkdir(path) +#endif + +#include "ingen/FilePath.hpp" + +/* A minimal subset of the std::filesystem API from C++17. */ + +namespace Ingen { +namespace filesystem { + +inline bool exists(const FilePath& path) +{ + return !access(path.c_str(), F_OK); +} + +inline bool is_directory(const FilePath& path) +{ + struct stat info; + stat(path.c_str(), &info); + return S_ISDIR(info.st_mode); +} + +inline bool create_directories(const FilePath& path) +{ + std::vector<FilePath> paths; + for (FilePath p = path; p != path.root_directory(); p = p.parent_path()) { + paths.emplace_back(p); + } + + for (auto p = paths.rbegin(); p != paths.rend(); ++p) { + if (mkdir(p->c_str(), 0755) && errno != EEXIST) { + return false; + } + } + + return true; +} + +inline FilePath current_path() +{ + struct Freer { void operator()(char* const ptr) { free(ptr); } }; + + std::unique_ptr<char, Freer> cpath(realpath(".", NULL)); + const FilePath path(cpath.get()); + return path; +} + +} // namespace filesystem +} // namespace Ingen + +#endif // INGEN_FILESYSTEM_HPP diff --git a/ingen/ingen.h b/ingen/ingen.h new file mode 100644 index 00000000..05b9e7b2 --- /dev/null +++ b/ingen/ingen.h @@ -0,0 +1,75 @@ +/* + This file is part of Ingen. + Copyright 2014-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_H +#define INGEN_H + +#ifdef INGEN_SHARED +# ifdef _WIN32 +# define INGEN_LIB_IMPORT __declspec(dllimport) +# define INGEN_LIB_EXPORT __declspec(dllexport) +# else +# define INGEN_LIB_IMPORT __attribute__((visibility("default"))) +# define INGEN_LIB_EXPORT __attribute__((visibility("default"))) +# endif +# ifdef INGEN_INTERNAL +# define INGEN_API INGEN_LIB_EXPORT +# else +# define INGEN_API INGEN_LIB_IMPORT +# endif +#else +# define INGEN_API +#endif + +#define INGEN_NS "http://drobilla.net/ns/ingen#" + +#define INGEN__Arc INGEN_NS "Arc" +#define INGEN__Block INGEN_NS "Block" +#define INGEN__BundleEnd INGEN_NS "BundleEnd" +#define INGEN__BundleStart INGEN_NS "BundleStart" +#define INGEN__Graph INGEN_NS "Graph" +#define INGEN__GraphPrototype INGEN_NS "GraphPrototype" +#define INGEN__Internal INGEN_NS "Internal" +#define INGEN__Node INGEN_NS "Node" +#define INGEN__Plugin INGEN_NS "Plugin" +#define INGEN__Redo INGEN_NS "Redo" +#define INGEN__Undo INGEN_NS "Undo" +#define INGEN__activity INGEN_NS "activity" +#define INGEN__arc INGEN_NS "arc" +#define INGEN__block INGEN_NS "block" +#define INGEN__broadcast INGEN_NS "broadcast" +#define INGEN__canvasX INGEN_NS "canvasX" +#define INGEN__canvasY INGEN_NS "canvasY" +#define INGEN__enabled INGEN_NS "enabled" +#define INGEN__externalContext INGEN_NS "externalContext" +#define INGEN__file INGEN_NS "file" +#define INGEN__head INGEN_NS "head" +#define INGEN__incidentTo INGEN_NS "incidentTo" +#define INGEN__internalContext INGEN_NS "internalContext" +#define INGEN__loadedBundle INGEN_NS "loadedBundle" +#define INGEN__maxRunLoad INGEN_NS "maxRunLoad" +#define INGEN__meanRunLoad INGEN_NS "meanRunLoad" +#define INGEN__minRunLoad INGEN_NS "minRunLoad" +#define INGEN__numThreads INGEN_NS "numThreads" +#define INGEN__polyphonic INGEN_NS "polyphonic" +#define INGEN__polyphony INGEN_NS "polyphony" +#define INGEN__prototype INGEN_NS "prototype" +#define INGEN__sprungLayout INGEN_NS "sprungLayout" +#define INGEN__tail INGEN_NS "tail" +#define INGEN__uiEmbedded INGEN_NS "uiEmbedded" +#define INGEN__value INGEN_NS "value" + +#endif // INGEN_H diff --git a/ingen/paths.hpp b/ingen/paths.hpp new file mode 100644 index 00000000..e75e71e0 --- /dev/null +++ b/ingen/paths.hpp @@ -0,0 +1,55 @@ +/* + 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_PATHS_HPP +#define INGEN_PATHS_HPP + +#include <cstddef> +#include <string> + +#include "ingen/URI.hpp" +#include "raul/Path.hpp" + +namespace Ingen { + +inline URI main_uri() { return URI("ingen:/main"); } + +inline bool uri_is_path(const URI& uri) +{ + const size_t root_len = main_uri().string().length(); + if (uri == main_uri()) { + return true; + } else { + return uri.string().substr(0, root_len + 1) == + main_uri().string() + "/"; + } +} + +inline Raul::Path uri_to_path(const URI& uri) +{ + return (uri == main_uri()) + ? Raul::Path("/") + : Raul::Path(uri.string().substr(main_uri().string().length())); +} + +inline URI path_to_uri(const Raul::Path& path) +{ + return URI(main_uri().string() + path.c_str()); +} + +} // namespace Ingen + +#endif // INGEN_PATHS_HPP diff --git a/ingen/runtime_paths.hpp b/ingen/runtime_paths.hpp new file mode 100644 index 00000000..1a8bc2c2 --- /dev/null +++ b/ingen/runtime_paths.hpp @@ -0,0 +1,42 @@ +/* + 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_RUNTIME_PATHS_HPP +#define INGEN_RUNTIME_PATHS_HPP + +#include <string> +#include <vector> + +#include "ingen/ingen.h" +#include "ingen/FilePath.hpp" + +namespace Ingen { + +extern const char search_path_separator; + +INGEN_API void set_bundle_path(const char* path); +INGEN_API void set_bundle_path_from_code(void* function); + +INGEN_API FilePath bundle_file_path(const std::string& name); +INGEN_API FilePath data_file_path(const std::string& name); +INGEN_API FilePath ingen_module_path(const std::string& name, FilePath dir={}); + +INGEN_API FilePath user_config_dir(); +INGEN_API std::vector<FilePath> system_config_dirs(); + +} // namespace Ingen + +#endif // INGEN_RUNTIME_PATHS_HPP diff --git a/ingen/types.hpp b/ingen/types.hpp new file mode 100644 index 00000000..7cd1c386 --- /dev/null +++ b/ingen/types.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_TYPES_HPP +#define INGEN_TYPES_HPP + +#include <cstdlib> +#include <memory> + +#include "raul/Maid.hpp" + +namespace Ingen { + +template <class T> +void NullDeleter(T* ptr) {} + +template <class T> +struct FreeDeleter { void operator()(T* const ptr) { free(ptr); } }; + +template <class T, class Deleter = std::default_delete<T>> +using UPtr = std::unique_ptr<T, Deleter>; + +template <class T> +using SPtr = std::shared_ptr<T>; + +template <class T> +using WPtr = std::weak_ptr<T>; + +template <class T> +using MPtr = Raul::managed_ptr<T>; + +template<class T, class U> +SPtr<T> static_ptr_cast(const SPtr<U>& r) { + return std::static_pointer_cast<T>(r); +} + +template<class T, class U> +SPtr<T> dynamic_ptr_cast(const SPtr<U>& r) { + return std::dynamic_pointer_cast<T>(r); +} + +template<class T, class U> +SPtr<T> const_ptr_cast(const SPtr<U>& r) { + return std::const_pointer_cast<T>(r); +} + +} // namespace Ingen + +#endif // INGEN_TYPES_HPP diff --git a/scripts/ingen.py b/scripts/ingen.py new file mode 100644 index 00000000..594b98cf --- /dev/null +++ b/scripts/ingen.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python +# Ingen Python Interface +# Copyright 2012-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. + +import os +import rdflib +import re +import socket +import sys + +try: + import StringIO.StringIO as StringIO +except ImportError: + from io import StringIO as StringIO + +class NS: + atom = rdflib.Namespace('http://lv2plug.in/ns/ext/atom#') + ingen = rdflib.Namespace('http://drobilla.net/ns/ingen#') + ingerr = rdflib.Namespace('http://drobilla.net/ns/ingen/errors#') + lv2 = rdflib.Namespace('http://lv2plug.in/ns/lv2core#') + patch = rdflib.Namespace('http://lv2plug.in/ns/ext/patch#') + rdf = rdflib.Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') + rsz = rdflib.Namespace('http://lv2plug.in/ns/ext/resize-port#') + xsd = rdflib.Namespace('http://www.w3.org/2001/XMLSchema#') + +class Interface: + 'The core Ingen interface' + def put(self, subject, body): + pass + + def patch(self, subject, remove, add): + pass + + def get(self, subject): + pass + + def set(self, subject, key, value): + pass + + def connect(self, tail, head): + pass + + def disconnect(self, tail, head): + pass + + def delete(self, subject): + pass + +class Error(Exception): + def __init__(self, msg, cause): + Exception.__init__(self, '%s; cause: %s' % (msg, cause)) + +def lv2_path(): + path = os.getenv('LV2_PATH') + if path: + return path + elif sys.platform == 'darwin': + return os.pathsep.join(['~/Library/Audio/Plug-Ins/LV2', + '~/.lv2', + '/usr/local/lib/lv2', + '/usr/lib/lv2', + '/Library/Audio/Plug-Ins/LV2']) + elif sys.platform == 'haiku': + return os.pathsep.join(['~/.lv2', + '/boot/common/add-ons/lv2']) + elif sys.platform == 'win32': + return os.pathsep.join([ + os.path.join(os.getenv('APPDATA'), 'LV2'), + os.path.join(os.getenv('COMMONPROGRAMFILES'), 'LV2')]) + else: + return os.pathsep.join(['~/.lv2', + '/usr/lib/lv2', + '/usr/local/lib/lv2']) + +def ingen_bundle_path(): + for d in lv2_path().split(os.pathsep): + bundle = os.path.abspath(os.path.join(d, 'ingen.lv2')) + if os.path.exists(bundle): + return bundle + return None + +class Remote(Interface): + def __init__(self, uri='unix:///tmp/ingen.sock'): + self.msg_id = 1 + self.server_base = uri + '/' + self.model = rdflib.Graph() + self.ns_manager = rdflib.namespace.NamespaceManager(self.model) + self.ns_manager.bind('server', self.server_base) + for (k, v) in NS.__dict__.items(): + self.ns_manager.bind(k, v) + if uri.startswith('unix://'): + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(uri[len('unix://'):]) + elif uri.startswith('tcp://'): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + parsed = re.split('[:/]', uri[len('tcp://'):]) + addr = (parsed[0], int(parsed[1])) + self.sock.connect(addr) + else: + raise Exception('Unsupported server URI `%s' % uri) + + # Parse error description from Ingen bundle for pretty printing + bundle = ingen_bundle_path() + if bundle: + self.model.parse(os.path.join(bundle, 'errors.ttl'), format='n3') + + def __del__(self): + self.sock.close() + + def _get_prefixes_string(self): + s = '' + for k, v in self.ns_manager.namespaces(): + s += '@prefix %s: <%s> .\n' % (k, v) + return s + + def msgencode(self, msg): + if sys.version_info[0] == 3: + return bytes(msg, 'utf-8') + else: + return msg + + def update_model(self, update): + for i in update.triples([None, NS.rdf.type, NS.patch.Put]): + put = i[0] + subject = update.value(put, NS.patch.subject, None) + body = update.value(put, NS.patch.body, None) + desc = {} + for i in update.triples([body, None, None]): + self.model.add([subject, i[1], i[2]]) + return update + + def uri_to_path(self, uri): + path = uri + if uri.startswith(self.server_base): + return uri[len(self.server_base)-1:] + return uri + + def recv(self): + 'Read from socket until a NULL terminator is received' + msg = u'' + while True: + c = self.sock.recv(1, 0).decode('utf-8') + if not c or ord(c[0]) == 0: # End of transmission + break + else: + msg += c[0] + return msg + + def blank_closure(self, graph, node): + def blank_walk(node, g): + for i in g.triples([node, None, None]): + if type(i[2]) == rdflib.BNode and i[2] != node: + yield i[2] + blank_walk(i[2], g) + + closure = [node] + for b in graph.transitiveClosure(blank_walk, node): + closure += [b] + + return closure + + def raise_error(self, code, cause): + klass = self.model.value(None, NS.ingerr.errorCode, rdflib.Literal(code)) + if not klass: + raise Error('error %d' % code, cause) + + fmt = self.model.value(klass, NS.ingerr.formatString, None) + if not fmt: + raise Error('%s' % klass, cause) + + raise Error(fmt, cause) + + def send(self, msg): + # Send message to server + payload = msg + if sys.version_info[0] == 3: + payload = bytes(msg, 'utf-8') + self.sock.send(self.msgencode(msg)) + + # Receive response and parse into a model + response_str = self._get_prefixes_string() + self.recv() + response_model = rdflib.Graph(namespace_manager=self.ns_manager) + + # Because rdflib has embarrassingly broken base URI resolution that + # just drops path components from the base URI entirely (seriously), + # unfortunate the real server base URI can not be used here. Use + # <ingen:/> instead to at least avoid complete nonsense + response_model.parse(StringIO(response_str), 'ingen:/', format='n3') + + # Add new prefixes to prepend to future responses because rdflib sucks + for line in response_str.split('\n'): + if line.startswith('@prefix'): + match = re.search('@prefix ([^:]*): <(.*)> *\.', line) + if match: + name = match.group(1) + uri = match.group(2) + self.ns_manager.bind(match.group(1), match.group(2)) + + # Handle response (though there should be only one) + blanks = [] + response_desc = [] + for i in response_model.triples([None, NS.rdf.type, NS.patch.Response]): + response = i[0] + subject = response_model.value(response, NS.patch.subject, None) + body = response_model.value(response, NS.patch.body, None) + + response_desc += [i] + blanks += [response] + if body != 0: + self.raise_error(int(body), msg) # Raise exception on server error + + # Find the blank node closure of all responses + blank_closure = [] + for b in blanks: + blank_closure += self.blank_closure(response_model, b) + + # Remove response descriptions from model + for b in blank_closure: + for t in response_model.triples([b, None, None]): + response_model.remove(t) + + # Remove triples describing responses from response model + for i in response_desc: + response_model.remove(i) + + # Update model with remaining information, e.g. patch:Put updates + return self.update_model(response_model) + + def get(self, subject): + return self.send(''' +[] + a patch:Get ; + patch:subject <%s> . +''' % subject) + + def put(self, subject, body): + return self.send(''' +[] + a patch:Put ; + patch:subject <%s> ; + patch:body [ +%s + ] . +''' % (subject, body)) + + def patch(self, subject, remove, add): + return self.send(''' +[] + a patch:Patch ; + patch:subject <%s> ; + patch:remove [ +%s + ] ; + patch:add [ +%s + ] . +''' % (subject, remove, add)) + + def set(self, subject, key, value): + return self.send(''' +[] + a patch:Set ; + patch:subject <%s> ; + patch:property <%s> ; + patch:value %s . +''' % (subject, key, value)) + + def connect(self, tail, head): + return self.send(''' +[] + a patch:Put ; + patch:subject <%s> ; + patch:body [ + a ingen:Arc ; + ingen:tail <%s> ; + ingen:head <%s> ; + ] . +''' % (os.path.commonprefix([tail, head]), tail, head)) + + def disconnect(self, tail, head): + return self.send(''' +[] + a patch:Delete ; + patch:body [ + a ingen:Arc ; + ingen:tail <%s> ; + ingen:head <%s> ; + ] . +''' % (tail, head)) + + def delete(self, subject): + return self.send(''' +[] + a patch:Delete ; + patch:subject <%s> . +''' % subject) diff --git a/scripts/ingenams b/scripts/ingenams new file mode 100755 index 00000000..a183586a --- /dev/null +++ b/scripts/ingenams @@ -0,0 +1,283 @@ +#!/usr/bin/env python +# Load an AlsaModularSynth patch file into Ingen +# Copyright 2012-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. + +import ingen +import rdflib +import rdflib.namespace +import sys + +ams_prefix = 'http://github.com/blablack/ams-lv2/' +fomp_prefix = 'http://drobilla.net/plugins/fomp/' +note_uri = 'http://drobilla.net/ns/ingen-internals#Note' + +class World: + def __init__(self, server_uri): + self.server_uri = server_uri + self.server = ingen.Remote(server_uri) + self.pending_arcs = [] + self.server.get('/') + self.mod_prototypes = {} + + def mod_sym(self, mod_id): + return 'mod%d' % int(mod_id) + + def add_block(self, mod_id, plugin_uri, x, y): + self.mod_prototypes[self.mod_sym(mod_id)] = plugin_uri + self.server.put('/' + self.mod_sym(mod_id), + ('\t\ta ingen:Block ;\n' + + 'lv2:prototype <%s> ;\n' % plugin_uri + + 'ingen:canvasX %f ;\n' % x + + 'ingen:canvasY %f' % y).replace('\n', '\n\t\t')) + + def add_arc(self, + head_port_id, tail_port_id, + head_mod_id, tail_mod_id, + jack_color, cable_color): + self.pending_arcs += [(head_port_id, tail_port_id, + head_mod_id, tail_mod_id, + jack_color, cable_color)] + + def get_ports(self, mod_uri, port_type): + ports = [] + for i in self.server.model.triples([None, ingen.NS.rdf.type, port_type]): + if str(i[0]).startswith(mod_uri + '/'): + if not [i[0], ingen.NS.rdf.type, ingen.NS.lv2.ControlPort] in self.server.model: + # Unfortunately ingen.NS.lv2.index is a method + index = self.server.model.value(i[0], ingen.NS.lv2['index'], None) + ports += [[int(index), i[0]]] + return ports + + def input_by_id(self, mod_id, port_id): + mod_uri = rdflib.URIRef(self.server.server_base + self.mod_sym(mod_id)) + + # Get all input ports on this module sorted by index + inputs = sorted(self.get_ports(mod_uri, ingen.NS.lv2.InputPort)) + + # Return the port_id'th port in the list + index = 0 + for i in inputs: + if index == int(port_id): + return i[1] + index += 1 + + return None + + def output_by_id(self, mod_id, port_id): + mod_uri = rdflib.URIRef(self.server.server_base + self.mod_sym(mod_id)) + + # Get all output ports on this module sorted by index + outputs = sorted(self.get_ports(mod_uri, ingen.NS.lv2.OutputPort)) + + port_index = int(port_id) + if world.mod_prototypes[self.mod_sym(mod_id)] == note_uri: + # Adapt MCV/ADVMCV port index to Note port index + port_mapping = [ 3, 0, 2, 4, 6, 5, -1, -1, -1, -1 ] + port_index = port_mapping[port_index] + if port_index == -1: + sys.stderr.write('warning: unsupported MCV port %d\n' % int(port_id)) + return + + # Return the port_id'th port in the list + if port_index < len(outputs): + return outputs[port_index][1] + + return None + + def create_arcs(self): + for (head_port_id, tail_port_id, + head_mod_id, tail_mod_id, + jack_color, cable_color) in self.pending_arcs: + print('%s:%s => %s:%s' % (tail_mod_id, tail_port_id, head_mod_id, head_port_id)) + try: + tail = self.output_by_id(tail_mod_id, tail_port_id) + head = self.input_by_id(head_mod_id, head_port_id) + if tail and head: + self.server.connect(self.server.uri_to_path(tail), + self.server.uri_to_path(head)) + except: + pass + +# Static enumeration of special module type IDs +class Special: + CUSTOM = 0 + LADSPA = 6 + MCV = 10 + SCMCV = 30 + SCQUANTIZER = 31 + ADVMCV = 35 + +# Module types list, indexed by numeric ID in file +# Except where otherwise commented, these correspond to internal modules, +# and the string is the suffix of the corresponding AMS LV2 plugin URI +module_types = [ + "custom", # 0 = custom (unsupported) + "vco", + "vca", + "lfo", + "delay", + "ringmod", + "ladspa", # 6 = LADSPA plugin + "pcmout", + "mix", + "vcf", + "mcv", + "env", + "seq", + "inv", + "noise", + "slew", + "quantizer", + "pcmin", + "cvs", + "sh", + "vcorgan", + "dynamicwaves", + "advenv", + "wavout", + "scope", + "spectrum", + "vcswitch", + "jackin", + "jackout", + "midiout", + "scmcv", # Scala module (different line format) + "scquantizer", # Scala module (different line format) + "stereomix", + "conv", + "vcenv", + "advmcv", + "function", + "vcpanning", + "vcenv2", + "vcdoubledecay", + "vquant", + "amp", + "ad", + "mphlfo", + "noise2", + "vco2" +] + +class Module: + def __init__(self, num, plugin_uri, properties={}): + self.num = num + self.plugin_uri = plugin_uri + self.properties = properties + self.ports = [] + +class Patch: + def __init__(self): + self.modules = [] + +def ladspa_module(world, mod_id, x, y, poly, lib, label): + lv2_uri = '' + # Kludge LADSPA library and label to LV2 URIs where applicable + if lib == 'blvco': + lv2_uri = fomp_prefix + label.lower().replace('-', '_') + elif lib == 'mvclpf24' or lib == 'mvchpf24': + lv2_uri = fomp_prefix + label.lower().replace('-', '') + elif lib == 'cs_chorus' or lib == 'cs_phaser': + lv2_uri = fomp_prefix + 'cs_' + label.lower().replace('+', '_') + + if lv2_uri: + world.add_block(mod_id, lv2_uri, x, y) + else: + print('MOD %3d LADSPA %s %s %s' % (mod_id, poly, lib, label)) + +def scala_module(world, mod_id, scala_name): + sys.stderr.write('warning: scala module %3d (%s) unsupported\n' % (d, scala_name)) + +def standard_module(world, mod_id, x, y, name, arg): + if name == 'vca': + if int(arg) > 0: + name += 'exp' + else: + name += 'lin' + elif name == 'mix': + name += 'er_%dch' % int(arg) + + lv2_uri = ams_prefix + name + world.add_block(mod_id, lv2_uri, x, y) + +def float_control(world, mod_id, port_index, value, + logarithmic, minimum, maximum, midi_sign): + #print('FLOAT CONTROL %s:%s = %s' % (mod_id, port_index, value)) + pass + +def control(world, mod_id, port_index, value, midi_sign): + #print('CONTROL %s:%s = %s' % (mod_id, port_index, value)) + pass + +if len(sys.argv) != 2 and len(sys.argv) != 3: + sys.stderr.write('Usage: %s AMS_PATCH_FILE [SERVER_URI]\n' % sys.argv[0]) + sys.exit(1) + +in_path = sys.argv[1] +server_uri = 'unix:///tmp/ingen.sock' +if len(sys.argv) == 3: + server_uri = sys.argv[2] + +world = World(server_uri) +in_file = open(in_path, 'r') + +in_comment = False +for l in in_file: + try: + expr = l.split() + if not expr: + continue + elif expr[0] == '#PARA#': + in_comment = True + elif in_comment and expr[0] == '#ARAP#': + in_comment = False + elif expr[0] == 'Module': + mod_type = int(expr[1]) + mod_id = int(expr[2]) + mod_x = int(expr[3]) + mod_y = int(expr[4]) + if mod_type > len(module_types): + sys.stderr.write('warning: unknown module type %d\n', mod_type) + elif mod_type == Special.CUSTOM: + sys.stderr.write('warning: custom module %d unsupported\n' % mod_id) + if mod_type == Special.LADSPA: + ladspa_module(world, mod_id, mod_x, mod_y, int(expr[5]), expr[6], expr[7]) + elif mod_type == Special.SCMCV or mod_type == Special.SCQUANTIZER: + scala_name = expr[5] + scala_module(world, mod_id, scala_name) + elif mod_type == Special.MCV or mod_type == Special.ADVMCV: + world.add_block(mod_id, note_uri, mod_x, mod_y) + else: + standard_module(world, mod_id, mod_x, mod_y, module_types[mod_type], expr[5]) + elif expr[0] == 'ColorP': + world.add_arc(expr[1], expr[2], expr[3], expr[4], + (expr[5], expr[6], expr[7]), + (expr[8], expr[9], expr[10])) + elif expr[0] == 'FSlider': + float_control(world, mod_id, + expr[2], expr[3], expr[4], expr[5], expr[6], expr[7]) + elif expr[0] == 'ISlider' or expr[0] == 'LSlider': + control(world, mod_id, expr[2], expr[3], expr[4]) + #else: + # sys.stderr.write('warning: unsupported form %s\n' % expr[0]) + except ingen.Error: + e = sys.exc_info()[1] + sys.stderr.write('ingen error: %s\n' % e.message) + +world.create_arcs() + +#print(world.server.model.serialize(format='n3')) + +in_file.close() diff --git a/scripts/ingenish b/scripts/ingenish new file mode 100755 index 00000000..97640645 --- /dev/null +++ b/scripts/ingenish @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# Ingen Interactive Shell +# Copyright 2011-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. + +import ingen +import os.path +import re +import shlex +import sys +import time +try: + import readline +except: + pass + +# Python 2 compatibility +try: + input = raw_input +except NameError: + pass + +def print_usage(): + print('''Usage: ingenish [OPTION]... [COMMAND [ARGUMENT]...] + +A command line interface to an Ingen server. A command can be given directly +on the command line, or when run with no arguments an interactive shell is +launched. + +Options: + -s ADDRESS The address of the Ingen server. Default is the local server + at unix:///tmp/ingen.sock but remote servers can be used with + an address like tcp:///my-ingen-server-host:16180 + +Commands: + put SUBJECT BODY + set SUBJECT KEY VALUE + connect TAIL HEAD + disconnect TAIL HEAD + patch SUBJECT REMOVE ADD + delete SUBJECT + help + exit + +Subjects are specified by URI, relative to the engine. The top level audio +graph has the path /main, so for example, a port on a block might have the +(relative) URI /main/osc/freq. + +Bodies are specified in fragments of Turtle, just as written in Ingen graph files. + +Example: + put /main/left_in 'a lv2:InputPort ; a lv2:AudioPort' + put /main/left_out 'a lv2:OutputPort ; a lv2:AudioPort' + put /main/tone 'a ingen:Block ; lv2:prototype <http://drobilla.net/plugins/mda/Shepard>' + put /main/combo 'a ingen:Block ; lv2:prototype <http://drobilla.net/plugins/mda/Combo>' + connect /main/left_in /main/tone/left_in + connect /main/tone/left_out /main/combo/left_in + connect /main/combo/left_out /main/left_out + set /main/tone/output ingen:value 0.7 +''') + +def run(cmd): + if cmd[0] == 'help': + print_usage() + elif cmd[0] == 'exit': + sys.exit(0) + elif cmd[0] == 'get' and len(cmd) == 2: + print(ingen.get(cmd[1]).serialize(format='n3')) + elif cmd[0] == 'put' and len(cmd) == 3: + return ingen.put(cmd[1], cmd[2]) + elif cmd[0] == 'patch' and len(cmd) == 4: + return ingen.patch(cmd[1], cmd[2], cmd[3]) + elif cmd[0] == 'set' and len(cmd) == 4: + return ingen.set(cmd[1], cmd[2], cmd[3]) + elif cmd[0] == 'connect' and len(cmd) == 3: + return ingen.connect(cmd[1], cmd[2]) + elif cmd[0] == 'disconnect' and len(cmd) == 3: + return ingen.disconnect(cmd[1], cmd[2]) + elif cmd[0] == 'delete' and len(cmd) == 2: + return ingen.delete(cmd[1]) + return False + +a = 1 +server = 'unix:///tmp/ingen.sock' +if len(sys.argv) > 1: + if sys.argv[a] == '-s': + server = sys.argv[a + 1] + a = a + 2 + elif sys.argv[a][0] == '-': + print_usage() + sys.exit(1) + +ingen = ingen.Remote(server) + +if len(sys.argv) - a == 0: + print('Ingen server at %s' % server) + while True: + try: + run(shlex.split(input('> '))) + except (EOFError, KeyboardInterrupt, SystemExit): + break + except: + print('error: %s' % sys.exc_info()[1]) +else: + try: + update = run(sys.argv[a:]) + if update: + print(update.serialize(format='n3')) + except: + print('error: %s' % sys.exc_info()[1]) diff --git a/src/AtomReader.cpp b/src/AtomReader.cpp new file mode 100644 index 00000000..218110e4 --- /dev/null +++ b/src/AtomReader.cpp @@ -0,0 +1,384 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <utility> + +#include "ingen/AtomReader.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Message.hpp" +#include "ingen/Node.hpp" +#include "ingen/URIMap.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "raul/Path.hpp" + +namespace Ingen { + +AtomReader::AtomReader(URIMap& map, URIs& uris, Log& log, Interface& iface) + : _map(map) + , _uris(uris) + , _log(log) + , _iface(iface) +{} + +void +AtomReader::get_atom(const LV2_Atom* in, Atom& out) +{ + if (in) { + if (in->type == _uris.atom_URID) { + const LV2_Atom_URID* urid = (const LV2_Atom_URID*)in; + const char* uri = _map.unmap_uri(urid->body); + if (uri) { + out = Atom(sizeof(int32_t), _uris.atom_URID, &urid->body); + } else { + _log.error(fmt("Unable to unmap URID %1%\n") % urid->body); + } + } else { + out = Atom(in->size, in->type, LV2_ATOM_BODY_CONST(in)); + } + } +} + +void +AtomReader::get_props(const LV2_Atom_Object* obj, + Ingen::Properties& props) +{ + if (obj->body.otype) { + const Atom type(sizeof(int32_t), _uris.atom_URID, &obj->body.otype); + props.emplace(_uris.rdf_type, type); + } + LV2_ATOM_OBJECT_FOREACH(obj, p) { + Atom val; + get_atom(&p->value, val); + props.emplace(URI(_map.unmap_uri(p->key)), val); + } +} + +boost::optional<URI> +AtomReader::atom_to_uri(const LV2_Atom* atom) +{ + if (!atom) { + return boost::optional<URI>(); + } else if (atom->type == _uris.atom_URI) { + const char* str = (const char*)LV2_ATOM_BODY_CONST(atom); + if (URI::is_valid(str)) { + return URI(str); + } else { + _log.warn(fmt("Invalid URI <%1%>\n") % str); + } + } else if (atom->type == _uris.atom_Path) { + const char* str = (const char*)LV2_ATOM_BODY_CONST(atom); + if (!strncmp(str, "file://", 5)) { + return URI(str); + } else { + return URI(std::string("file://") + str); + } + } else if (atom->type == _uris.atom_URID) { + const char* str = _map.unmap_uri(((const LV2_Atom_URID*)atom)->body); + if (str) { + return URI(str); + } else { + _log.warn(fmt("Unknown URID %1%\n") % str); + } + } + return boost::optional<URI>(); +} + +boost::optional<Raul::Path> +AtomReader::atom_to_path(const LV2_Atom* atom) +{ + boost::optional<URI> uri = atom_to_uri(atom); + if (uri && uri_is_path(*uri)) { + return uri_to_path(*uri); + } + return boost::optional<Raul::Path>(); +} + +Resource::Graph +AtomReader::atom_to_context(const LV2_Atom* atom) +{ + Resource::Graph ctx = Resource::Graph::DEFAULT; + if (atom) { + boost::optional<URI> maybe_uri = atom_to_uri(atom); + if (maybe_uri) { + ctx = Resource::uri_to_graph(*maybe_uri); + } else { + _log.warn("Message has invalid context\n"); + } + } + return ctx; +} + +bool +AtomReader::is_message(const URIs& uris, const LV2_Atom* msg) +{ + if (msg->type != uris.atom_Object) { + return false; + } + + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)msg; + return (obj->body.otype == uris.patch_Get || + obj->body.otype == uris.patch_Delete || + obj->body.otype == uris.patch_Put || + obj->body.otype == uris.patch_Set || + obj->body.otype == uris.patch_Patch || + obj->body.otype == uris.patch_Move || + obj->body.otype == uris.patch_Response); +} + +bool +AtomReader::write(const LV2_Atom* msg, int32_t default_id) +{ + if (msg->type != _uris.atom_Object) { + _log.warn(fmt("Unknown message type <%1%>\n") + % _map.unmap_uri(msg->type)); + return false; + } + + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)msg; + const LV2_Atom* subject = nullptr; + const LV2_Atom* number = nullptr; + + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_subject, &subject, + (LV2_URID)_uris.patch_sequenceNumber, &number, + NULL); + + const boost::optional<URI> subject_uri = atom_to_uri(subject); + + const int32_t seq = ((number && number->type == _uris.atom_Int) + ? ((const LV2_Atom_Int*)number)->body + : default_id); + + if (obj->body.otype == _uris.patch_Get) { + if (subject_uri) { + _iface(Get{seq, *subject_uri}); + } + } else if (obj->body.otype == _uris.ingen_BundleStart) { + _iface(BundleBegin{seq}); + } else if (obj->body.otype == _uris.ingen_BundleEnd) { + _iface(BundleEnd{seq}); + } else if (obj->body.otype == _uris.patch_Delete) { + const LV2_Atom_Object* body = nullptr; + lv2_atom_object_get(obj, (LV2_URID)_uris.patch_body, &body, 0); + + if (subject_uri && !body) { + _iface(Del{seq, *subject_uri}); + return true; + } else if (body && body->body.otype == _uris.ingen_Arc) { + const LV2_Atom* tail = nullptr; + const LV2_Atom* head = nullptr; + const LV2_Atom* incidentTo = nullptr; + lv2_atom_object_get(body, + (LV2_URID)_uris.ingen_tail, &tail, + (LV2_URID)_uris.ingen_head, &head, + (LV2_URID)_uris.ingen_incidentTo, &incidentTo, + NULL); + + boost::optional<Raul::Path> subject_path(atom_to_path(subject)); + boost::optional<Raul::Path> tail_path(atom_to_path(tail)); + boost::optional<Raul::Path> head_path(atom_to_path(head)); + boost::optional<Raul::Path> other_path(atom_to_path(incidentTo)); + if (tail_path && head_path) { + _iface(Disconnect{seq, *tail_path, *head_path}); + } else if (subject_path && other_path) { + _iface(DisconnectAll{seq, *subject_path, *other_path}); + } else { + _log.warn("Delete of unknown object\n"); + return false; + } + } + } else if (obj->body.otype == _uris.patch_Put) { + const LV2_Atom_Object* body = nullptr; + const LV2_Atom* context = nullptr; + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_body, &body, + (LV2_URID)_uris.patch_context, &context, + 0); + if (!body) { + _log.warn("Put message has no body\n"); + return false; + } else if (!subject_uri) { + _log.warn("Put message has no subject\n"); + return false; + } + + if (body->body.otype == _uris.ingen_Arc) { + LV2_Atom* tail = nullptr; + LV2_Atom* head = nullptr; + lv2_atom_object_get(body, + (LV2_URID)_uris.ingen_tail, &tail, + (LV2_URID)_uris.ingen_head, &head, + NULL); + if (!tail || !head) { + _log.warn("Arc has no tail or head\n"); + return false; + } + + boost::optional<Raul::Path> tail_path(atom_to_path(tail)); + boost::optional<Raul::Path> head_path(atom_to_path(head)); + if (tail_path && head_path) { + _iface(Connect{seq, *tail_path, *head_path}); + } else { + _log.warn("Arc has non-path tail or head\n"); + } + } else { + Ingen::Properties props; + get_props(body, props); + _iface(Put{seq, *subject_uri, props, atom_to_context(context)}); + } + } else if (obj->body.otype == _uris.patch_Set) { + if (!subject_uri) { + _log.warn("Set message has no subject\n"); + return false; + } + + const LV2_Atom_URID* prop = nullptr; + const LV2_Atom* value = nullptr; + const LV2_Atom* context = nullptr; + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_property, &prop, + (LV2_URID)_uris.patch_value, &value, + (LV2_URID)_uris.patch_context, &context, + 0); + if (!prop || ((const LV2_Atom*)prop)->type != _uris.atom_URID) { + _log.warn("Set message missing property\n"); + return false; + } else if (!value) { + _log.warn("Set message missing value\n"); + return false; + } + + Atom atom; + get_atom(value, atom); + _iface(SetProperty{seq, + *subject_uri, + URI(_map.unmap_uri(prop->body)), + atom, + atom_to_context(context)}); + } else if (obj->body.otype == _uris.patch_Patch) { + if (!subject_uri) { + _log.warn("Patch message has no subject\n"); + return false; + } + + const LV2_Atom_Object* remove = nullptr; + const LV2_Atom_Object* add = nullptr; + const LV2_Atom* context = nullptr; + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_remove, &remove, + (LV2_URID)_uris.patch_add, &add, + (LV2_URID)_uris.patch_context, &context, + 0); + if (!remove) { + _log.warn("Patch message has no remove\n"); + return false; + } else if (!add) { + _log.warn("Patch message has no add\n"); + return false; + } + + Ingen::Properties add_props; + get_props(add, add_props); + + Ingen::Properties remove_props; + get_props(remove, remove_props); + + _iface(Delta{seq, *subject_uri, remove_props, add_props, + atom_to_context(context)}); + } else if (obj->body.otype == _uris.patch_Copy) { + if (!subject) { + _log.warn("Copy message has no subject\n"); + return false; + } + + const LV2_Atom* dest = nullptr; + lv2_atom_object_get(obj, (LV2_URID)_uris.patch_destination, &dest, 0); + if (!dest) { + _log.warn("Copy message has no destination\n"); + return false; + } + + boost::optional<URI> subject_uri(atom_to_uri(subject)); + if (!subject_uri) { + _log.warn("Copy message has non-path subject\n"); + return false; + } + + boost::optional<URI> dest_uri(atom_to_uri(dest)); + if (!dest_uri) { + _log.warn("Copy message has non-URI destination\n"); + return false; + } + + _iface(Copy{seq, *subject_uri, *dest_uri}); + } else if (obj->body.otype == _uris.patch_Move) { + if (!subject) { + _log.warn("Move message has no subject\n"); + return false; + } + + const LV2_Atom* dest = nullptr; + lv2_atom_object_get(obj, (LV2_URID)_uris.patch_destination, &dest, 0); + if (!dest) { + _log.warn("Move message has no destination\n"); + return false; + } + + boost::optional<Raul::Path> subject_path(atom_to_path(subject)); + if (!subject_path) { + _log.warn("Move message has non-path subject\n"); + return false; + } + + boost::optional<Raul::Path> dest_path(atom_to_path(dest)); + if (!dest_path) { + _log.warn("Move message has non-path destination\n"); + return false; + } + + _iface(Move{seq, *subject_path, *dest_path}); + } else if (obj->body.otype == _uris.patch_Response) { + const LV2_Atom* seq = nullptr; + const LV2_Atom* body = nullptr; + lv2_atom_object_get(obj, + (LV2_URID)_uris.patch_sequenceNumber, &seq, + (LV2_URID)_uris.patch_body, &body, + 0); + if (!seq || seq->type != _uris.atom_Int) { + _log.warn("Response message has no sequence number\n"); + return false; + } else if (!body || body->type != _uris.atom_Int) { + _log.warn("Response message body is not integer\n"); + return false; + } + _iface(Response{((const LV2_Atom_Int*)seq)->body, + (Ingen::Status)((const LV2_Atom_Int*)body)->body, + subject_uri ? subject_uri->c_str() : ""}); + } else if (obj->body.otype == _uris.ingen_BundleStart) { + _iface(BundleBegin{seq}); + } else if (obj->body.otype == _uris.ingen_BundleEnd) { + _iface(BundleEnd{seq}); + } else { + _log.warn(fmt("Unknown object type <%1%>\n") + % _map.unmap_uri(obj->body.otype)); + } + + return true; +} + +} // namespace Ingen diff --git a/src/AtomWriter.cpp b/src/AtomWriter.cpp new file mode 100644 index 00000000..f235aafc --- /dev/null +++ b/src/AtomWriter.cpp @@ -0,0 +1,652 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +/** @page protocol Ingen Protocol + * @tableofcontents + * + * @section introduction Controlling Ingen + * + * Ingen is controlled via a simple RDF-based protocol. This conceptual + * protocol can be used in two concrete ways: + * + * 1. When Ingen is running as a process, a socket accepts messages in + * (textual) Turtle syntax, and responds in the same syntax. Transfers are + * delimited by NULL characters in the stream, so the client knows when to + * finish parsing to interpret responses. By default, Ingen listens on + * unix:///tmp/ingen.sock and tcp://localhost:16180 + * + * 2. When Ingen is running as an LV2 plugin, the control and notify ports + * accept and respond using (binary) LV2 atoms. Messages are read and written + * as events with atom:Object bodies. The standard rules for LV2 event + * transmission apply, but note that the graph manipulations described here are + * executed asynchronously and not with sample accuracy, so the response may + * come at a later frame or cycle. + * + * For documentation purposes, this page discusses messages in Turtle syntax, + * but the same protocol is used in the LV2 case, just in a more compact binary + * encoding. + * + * Conceptually, Ingen represents a tree of objects, each of which has a path + * (like /main/in or /main/osc/out) and a set of properties. A property is a + * URI key and some value. All changes to Ingen are represented as + * manipulations of this tree of dictionaries. The core of the protocol is + * based on the LV2 patch extension, which defines several messages for + * manipulating data in this model which resemble HTTP methods. + */ + +#include <cassert> +#include <cstdlib> +#include <string> + +#include <boost/variant/apply_visitor.hpp> + +#include "ingen/AtomSink.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/Node.hpp" +#include "ingen/URIMap.hpp" +#include "raul/Path.hpp" +#include "serd/serd.h" + +namespace Ingen { + +AtomWriter::AtomWriter(URIMap& map, URIs& uris, AtomSink& sink) + : _map(map) + , _uris(uris) + , _sink(sink) +{ + lv2_atom_forge_init(&_forge, &map.urid_map_feature()->urid_map); + _out.set_forge_sink(&_forge); +} + +void +AtomWriter::finish_msg() +{ + assert(!_forge.stack); + _sink.write(_out.atom()); + _out.clear(); +} + +void +AtomWriter::message(const Message& message) +{ + boost::apply_visitor(*this, message); +} + +/** @page protocol + * @subsection Bundles + * + * An [ingen:BundleStart](http://drobilla.net/ns/ingen#BundleStart) marks the + * start of a bundle in the operation stream. A bundle groups a series of + * messages for coarse-grained undo or atomic execution. + * + * @code{.ttl} + * [] a ingen:BundleStart . + * @endcode + + * An [ingen:BundleEnd](http://drobilla.net/ns/ingen#BundleEnd) marks the end + * of a bundle in the operation stream. + * + * @code{.ttl} + * [] a ingen:BundleEnd . + * @endcode + */ +void +AtomWriter::operator()(const BundleBegin& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.ingen_BundleStart, message.seq); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +void +AtomWriter::operator()(const BundleEnd& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.ingen_BundleEnd, message.seq); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +void +AtomWriter::forge_uri(const URI& uri) +{ + if (serd_uri_string_has_scheme((const uint8_t*)uri.c_str())) { + lv2_atom_forge_urid(&_forge, _map.map_uri(uri.c_str())); + } else { + lv2_atom_forge_uri(&_forge, uri.c_str(), uri.length()); + } +} + +void +AtomWriter::forge_properties(const Properties& properties) +{ + for (auto p : properties) { + lv2_atom_forge_key(&_forge, _map.map_uri(p.first.c_str())); + if (p.second.type() == _forge.URI) { + forge_uri(URI(p.second.ptr<char>())); + } else { + lv2_atom_forge_atom(&_forge, p.second.size(), p.second.type()); + lv2_atom_forge_write(&_forge, p.second.get_body(), p.second.size()); + } + } +} + +void +AtomWriter::forge_arc(const Raul::Path& tail, const Raul::Path& head) +{ + LV2_Atom_Forge_Frame arc; + lv2_atom_forge_object(&_forge, &arc, 0, _uris.ingen_Arc); + lv2_atom_forge_key(&_forge, _uris.ingen_tail); + forge_uri(path_to_uri(tail)); + lv2_atom_forge_key(&_forge, _uris.ingen_head); + forge_uri(path_to_uri(head)); + lv2_atom_forge_pop(&_forge, &arc); +} + +void +AtomWriter::forge_request(LV2_Atom_Forge_Frame* frame, LV2_URID type, int32_t id) +{ + lv2_atom_forge_object(&_forge, frame, 0, type); + + if (id) { + lv2_atom_forge_key(&_forge, _uris.patch_sequenceNumber); + lv2_atom_forge_int(&_forge, id); + } +} + +void +AtomWriter::forge_context(Resource::Graph ctx) +{ + switch (ctx) { + case Resource::Graph::EXTERNAL: + lv2_atom_forge_key(&_forge, _uris.patch_context); + forge_uri(_uris.ingen_externalContext); + break; + case Resource::Graph::INTERNAL: + lv2_atom_forge_key(&_forge, _uris.patch_context); + forge_uri(_uris.ingen_internalContext); + break; + default: break; + } +} + +/** @page protocol + * @section methods Methods + * @subsection Put + * + * Send a [Put](http://lv2plug.in/ns/ext/patch#Put) to set properties on an + * object, creating it if necessary. + * + * If the object already exists, all existing object properties with keys that + * match any property in the message are first removed. Other properties are + * left unchanged. + * + * If the object does not yet exist, the message must contain sufficient + * information to create it, including at least one rdf:type property. + * + * @code{.ttl} + * [] + * a patch:Put ; + * patch:subject </main/osc> ; + * patch:body [ + * a ingen:Block ; + * lv2:prototype <http: //drobilla.net/plugins/mda/Shepard> + * ] . + * @endcode + */ +void +AtomWriter::operator()(const Put& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Put, message.seq); + forge_context(message.ctx); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.uri); + lv2_atom_forge_key(&_forge, _uris.patch_body); + + LV2_Atom_Forge_Frame body; + lv2_atom_forge_object(&_forge, &body, 0, 0); + forge_properties(message.properties); + lv2_atom_forge_pop(&_forge, &body); + + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Patch + * + * Send a [Patch](http://lv2plug.in/ns/ext/patch#Patch) to manipulate the + * properties of an object. A set of properties are first removed, then + * another is added. Analogous to WebDAV PROPPATCH. + * + * The special value [patch:wildcard](http://lv2plug.in/ns/ext/patch#wildcard) + * may be used to specify that any value with the given key should be removed. + * + * @code{.ttl} + * [] + * a patch:Patch ; + * patch:subject </main/osc> ; + * patch:add [ + * lv2:name "Osckillator" ; + * ingen:canvasX 32.0 ; + * ingen:canvasY 32.0 ; + * ] ; + * patch:remove [ + * eg:name "Old name" ; # Remove specific value + * ingen:canvasX patch:wildcard ; # Remove all + * ingen:canvasY patch:wildcard ; # Remove all + * ] . + * @endcode + */ +void +AtomWriter::operator()(const Delta& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Patch, message.seq); + forge_context(message.ctx); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.uri); + + lv2_atom_forge_key(&_forge, _uris.patch_remove); + LV2_Atom_Forge_Frame remove_obj; + lv2_atom_forge_object(&_forge, &remove_obj, 0, 0); + forge_properties(message.remove); + lv2_atom_forge_pop(&_forge, &remove_obj); + + lv2_atom_forge_key(&_forge, _uris.patch_add); + LV2_Atom_Forge_Frame add_obj; + lv2_atom_forge_object(&_forge, &add_obj, 0, 0); + forge_properties(message.add); + lv2_atom_forge_pop(&_forge, &add_obj); + + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Copy + * + * Send a [Copy](http://lv2plug.in/ns/ext/copy#Copy) to copy an object from + * its current location (subject) to another (destination). + * + * If both the subject and destination are inside Ingen, like block paths, then the old object + * is copied by, for example, creating a new plugin instance. + * + * If the subject is a filename (file URI or atom:Path) and the destination is + * inside Ingen, then the subject must be an Ingen graph file or bundle, which + * is loaded to the specified destination path. + * + * If the subject is inside Ingen and the destination is a filename, then the + * subject is saved to an Ingen bundle at the given destination. + * + * @code{.ttl} + * [] + * a patch:Copy ; + * patch:subject </main/osc> ; + * patch:destination </main/osc2> . + * @endcode + */ +void +AtomWriter::operator()(const Copy& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Copy, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.old_uri); + lv2_atom_forge_key(&_forge, _uris.patch_destination); + forge_uri(message.new_uri); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Move + * + * Send a [Move](http://lv2plug.in/ns/ext/move#Move) to move an object from its + * current location (subject) to another (destination). + * + * Both subject and destination must be paths in Ingen with the same parent, + * moving between graphs is currently not supported. + * + * @code{.ttl} + * [] + * a patch:Move ; + * patch:subject </main/osc> ; + * patch:destination </main/osc2> . + * @endcode + */ +void +AtomWriter::operator()(const Move& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Move, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(path_to_uri(message.old_path)); + lv2_atom_forge_key(&_forge, _uris.patch_destination); + forge_uri(path_to_uri(message.new_path)); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Delete + * + * Send a [Delete](http://lv2plug.in/ns/ext/delete#Delete) to remove an + * object from the engine and destroy it. + * + * All properties of the object are lost, as are all references to the object + * (e.g. any connections to it). + * + * @code{.ttl} + * [] + * a patch:Delete ; + * patch:subject </main/osc> . + * @endcode + */ +void +AtomWriter::operator()(const Del& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Delete, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.uri); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Set + * + * Send a [Set](http://lv2plug.in/ns/ext/patch#Set) to set a property on an + * object. Any existing value for that property is removed. + * + * @code{.ttl} + * [] + * a patch:Set ; + * patch:subject </main/osc> ; + * patch:property lv2:name ; + * patch:value "Oscwellator" . + * @endcode + */ +void +AtomWriter::operator()(const SetProperty& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Set, message.seq); + forge_context(message.ctx); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.subject); + lv2_atom_forge_key(&_forge, _uris.patch_property); + lv2_atom_forge_urid(&_forge, _map.map_uri(message.predicate.c_str())); + lv2_atom_forge_key(&_forge, _uris.patch_value); + lv2_atom_forge_atom(&_forge, message.value.size(), message.value.type()); + lv2_atom_forge_write(&_forge, message.value.get_body(), message.value.size()); + + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Undo + * + * Use [ingen:Undo](http://drobilla.net/ns/ingen#Undo) to undo the last change + * to the engine. Undo transactions can be delimited using bundle markers, if + * the last operations(s) received were in a bundle, then an Undo will undo the + * effects of that entire bundle. + * + * @code{.ttl} + * [] a ingen:Undo . + * @endcode + */ +void +AtomWriter::operator()(const Undo& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.ingen_Undo, message.seq); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Undo + * + * Use [ingen:Redo](http://drobilla.net/ns/ingen#Redo) to redo the last undone change. + * + * @code{.ttl} + * [] a ingen:Redo . + * @endcode + */ +void +AtomWriter::operator()(const Redo& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.ingen_Redo, message.seq); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Get + * + * Send a [Get](http://lv2plug.in/ns/ext/patch#Get) to get the description + * of the subject. + * + * @code{.ttl} + * [] + * a patch:Get ; + * patch:subject </main/osc> . + * @endcode + */ +void +AtomWriter::operator()(const Get& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Get, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(message.subject); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * + * @section arcs Arc Manipulation + */ + +/** @page protocol + * @subsection Connect Connecting Ports + * + * Ports are connected by putting an arc with the desired tail (an output port) + * and head (an input port). The tail and head must both be within the + * subject, which must be a graph. + * + * @code{.ttl} + * [] + * a patch:Put ; + * patch:subject </main/> ; + * patch:body [ + * a ingen:Arc ; + * ingen:tail </main/osc/out> ; + * ingen:head </main/filt/in> ; + * ] . + * @endcode + */ +void +AtomWriter::operator()(const Connect& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Put, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(path_to_uri(Raul::Path::lca(message.tail, message.head))); + lv2_atom_forge_key(&_forge, _uris.patch_body); + forge_arc(message.tail, message.head); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection Disconnect Disconnecting Ports + * + * Ports are disconnected by deleting the arc between them. The description of + * the arc is the same as in the put command used to create the connection. + * + * @code{.ttl} + * [] + * a patch:Delete ; + * patch:body [ + * a ingen:Arc ; + * ingen:tail </main/osc/out> ; + * ingen:head </main/filt/in> ; + * ] . + * @endcode + */ +void +AtomWriter::operator()(const Disconnect& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Delete, message.seq); + lv2_atom_forge_key(&_forge, _uris.patch_body); + forge_arc(message.tail, message.head); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @subsection DisconnectAll Fully Disconnecting Anything + * + * All arcs to or from anything can be removed by giving the special property + * ingen:incidentTo rather than a specific head and tail. This works for both + * ports and blocks (where the effect is to disconnect everything from ports on + * that block). + * + * @code{.ttl} + * [] + * a patch:Delete ; + * patch:subject </main> ; + * patch:body [ + * a ingen:Arc ; + * ingen:incidentTo </main/osc/out> + * ] . + * @endcode + */ +void +AtomWriter::operator()(const DisconnectAll& message) +{ + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Delete, message.seq); + + lv2_atom_forge_key(&_forge, _uris.patch_subject); + forge_uri(path_to_uri(message.graph)); + + lv2_atom_forge_key(&_forge, _uris.patch_body); + LV2_Atom_Forge_Frame arc; + lv2_atom_forge_object(&_forge, &arc, 0, _uris.ingen_Arc); + lv2_atom_forge_key(&_forge, _uris.ingen_incidentTo); + forge_uri(path_to_uri(message.path)); + lv2_atom_forge_pop(&_forge, &arc); + + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +/** @page protocol + * @section Responses + * + * Ingen responds to requests if the patch:sequenceNumber property is set. For + * example: + * @code{.ttl} + * [] + * a patch:Get ; + * patch:sequenceNumber 42 ; + * patch:subject </main/osc> . + * @endcode + * + * Might receive a response like: + * @code{.ttl} + * [] + * a patch:Response ; + * patch:sequenceNumber 42 ; + * patch:subject </main/osc> ; + * patch:body 0 . + * @endcode + * + * Where 0 is a status code, 0 meaning success and any other value being an + * error. Information about status codes, including error message strings, + * are defined in ingen.lv2/errors.ttl. + * + * Note that a response is only a status response, operations that manipulate + * the graph may generate new data on the stream, e.g. the above get request + * would also receive a put that describes /main/osc in the stream immediately + * following the response. + */ +void +AtomWriter::operator()(const Response& response) +{ + const auto& subject = response.subject; + if (!response.id) { + return; + } + + LV2_Atom_Forge_Frame msg; + forge_request(&msg, _uris.patch_Response, 0); + lv2_atom_forge_key(&_forge, _uris.patch_sequenceNumber); + lv2_atom_forge_int(&_forge, response.id); + if (!subject.empty()) { + lv2_atom_forge_key(&_forge, _uris.patch_subject); + lv2_atom_forge_uri(&_forge, subject.c_str(), subject.length()); + } + lv2_atom_forge_key(&_forge, _uris.patch_body); + lv2_atom_forge_int(&_forge, static_cast<int>(response.status)); + lv2_atom_forge_pop(&_forge, &msg); + finish_msg(); +} + +void +AtomWriter::operator()(const Error&) +{ +} + +/** @page protocol + * @section loading Loading and Unloading Bundles + * + * The property ingen:loadedBundle on the engine can be used to load + * or unload bundles from Ingen's world. For example: + * + * @code{.ttl} + * # Load /old.lv2 + * [] + * a patch:Put ; + * patch:subject </> ; + * patch:body [ + * ingen:loadedBundle <file:///old.lv2/> + * ] . + * + * # Replace /old.lv2 with /new.lv2 + * [] + * a patch:Patch ; + * patch:subject </> ; + * patch:remove [ + * ingen:loadedBundle <file:///old.lv2/> + * ]; + * patch:add [ + * ingen:loadedBundle <file:///new.lv2/> + * ] . + * @endcode + */ + +} // namespace Ingen diff --git a/src/ClashAvoider.cpp b/src/ClashAvoider.cpp new file mode 100644 index 00000000..ae4438a4 --- /dev/null +++ b/src/ClashAvoider.cpp @@ -0,0 +1,136 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cstdio> +#include <sstream> +#include <string> +#include <utility> + +#include "ingen/ClashAvoider.hpp" +#include "ingen/Store.hpp" +#include "ingen/paths.hpp" + +namespace Ingen { + +ClashAvoider::ClashAvoider(const Store& store) + : _store(store) +{} + +const URI +ClashAvoider::map_uri(const URI& in) +{ + if (uri_is_path(in)) { + return path_to_uri(map_path(uri_to_path(in))); + } else { + return in; + } +} + +const Raul::Path +ClashAvoider::map_path(const Raul::Path& in) +{ + unsigned offset = 0; + bool has_offset = false; + const size_t pos = in.find_last_of('_'); + if (pos != std::string::npos && pos != (in.length()-1)) { + const std::string trailing = in.substr(pos + 1); + has_offset = (sscanf(trailing.c_str(), "%u", &offset) > 0); + } + + // Path without _n suffix + std::string base_path_str = in; + if (has_offset) { + base_path_str = base_path_str.substr(0, base_path_str.find_last_of('_')); + } + + Raul::Path base_path(base_path_str); + + auto m = _symbol_map.find(in); + if (m != _symbol_map.end()) { + return m->second; + } else { + typedef std::pair<SymbolMap::iterator, bool> InsertRecord; + + // See if parent is mapped + Raul::Path parent = in.parent(); + do { + auto p = _symbol_map.find(parent); + if (p != _symbol_map.end()) { + const Raul::Path mapped = Raul::Path( + p->second.base() + in.substr(parent.base().length())); + InsertRecord i = _symbol_map.emplace(in, mapped); + return i.first->second; + } + parent = parent.parent(); + } while (!parent.is_root()); + + if (!exists(in) && _symbol_map.find(in) == _symbol_map.end()) { + // No clash, use symbol unmodified + InsertRecord i = _symbol_map.emplace(in, in); + assert(i.second); + return i.first->second; + + } else { + // Append _2 _3 etc until an unused symbol is found + while (true) { + auto o = _offsets.find(base_path); + if (o != _offsets.end()) { + offset = ++o->second; + } else { + std::string parent_str = in.parent().base(); + parent_str = parent_str.substr(0, parent_str.find_last_of("/")); + if (parent_str.empty()) { + parent_str = "/"; + } + } + + if (offset == 0) { + offset = 2; + } + + std::stringstream ss; + ss << base_path << "_" << offset; + if (!exists(Raul::Path(ss.str()))) { + std::string name = base_path.symbol(); + if (name == "") { + name = "_"; + } + Raul::Symbol sym(name); + std::string str = ss.str(); + InsertRecord i = _symbol_map.emplace(in, Raul::Path(str)); + offset = _store.child_name_offset(in.parent(), sym, false); + _offsets.emplace(base_path, offset); + return i.first->second; + } else { + if (o != _offsets.end()) { + offset = ++o->second; + } else { + ++offset; + } + } + } + } + } +} + +bool +ClashAvoider::exists(const Raul::Path& path) const +{ + return _store.find(path) != _store.end(); +} + +} // namespace Ingen diff --git a/src/ColorContext.cpp b/src/ColorContext.cpp new file mode 100644 index 00000000..23b568f1 --- /dev/null +++ b/src/ColorContext.cpp @@ -0,0 +1,44 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/ColorContext.hpp" +#include "ingen_config.h" + +#ifdef HAVE_ISATTY +# include <unistd.h> +#else +inline int isatty(int fd) { return 0; } +#endif + +namespace Ingen { + +ColorContext::ColorContext(FILE* stream, Color color) + : _stream(stream) +{ + if (isatty(fileno(_stream))) { + fprintf(_stream, "\033[0;%dm", (int)color); + } +} + +ColorContext::~ColorContext() +{ + if (isatty(fileno(_stream))) { + fprintf(_stream, "\033[0m"); + fflush(_stream); + } +} + +} // namespace Ingen diff --git a/src/Configuration.cpp b/src/Configuration.cpp new file mode 100644 index 00000000..c797cf93 --- /dev/null +++ b/src/Configuration.cpp @@ -0,0 +1,386 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cerrno> +#include <cstring> +#include <iostream> +#include <thread> + +#include "ingen/Configuration.hpp" +#include "ingen/Forge.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/filesystem.hpp" +#include "ingen/ingen.h" +#include "ingen/runtime_paths.hpp" +#include "sord/sordmm.hpp" +#include "sratom/sratom.h" + +namespace Ingen { + +Configuration::Configuration(Forge& forge) + : _forge(forge) + , _shortdesc("A realtime modular audio processor.") + , _desc( + "Ingen is a flexible modular system that be used in various ways.\n" + "The engine can run as a server controlled via a network protocol,\n" + "as an LV2 plugin, or in a monolithic process with a GUI. The GUI\n" + "may be run separately to control a remote engine, and many clients\n" + "may connect to an engine at once.\n\n" + "Examples:\n" + " ingen -e # Run engine, listen for connections\n" + " ingen -g # Run GUI, connect to running engine\n" + " ingen -eg # Run engine and GUI in one process\n" + " ingen -eg foo.ingen # Run engine and GUI and load a graph") + , _max_name_length(0) +{ + add("atomicBundles", "atomic-bundles", 'a', "Execute bundles atomically", GLOBAL, forge.Bool, forge.make(false)); + add("bufferSize", "buffer-size", 'b', "Buffer size in samples", GLOBAL, forge.Int, forge.make(1024)); + add("clientPort", "client-port", 'C', "Client port", GLOBAL, forge.Int, Atom()); + add("connect", "connect", 'c', "Connect to engine URI", SESSION, forge.String, forge.alloc("unix:///tmp/ingen.sock")); + add("engine", "engine", 'e', "Run (JACK) engine", SESSION, forge.Bool, forge.make(false)); + add("enginePort", "engine-port", 'E', "Engine listen port", GLOBAL, forge.Int, forge.make(16180)); + add("socket", "socket", 'S', "Engine socket path", GLOBAL, forge.String, forge.alloc("/tmp/ingen.sock")); + add("gui", "gui", 'g', "Launch the GTK graphical interface", SESSION, forge.Bool, forge.make(false)); + add("", "help", 'h', "Print this help message", SESSION, forge.Bool, forge.make(false)); + add("", "version", 'V', "Print version information", SESSION, forge.Bool, forge.make(false)); + add("jackName", "jack-name", 'n', "JACK name", GLOBAL, forge.String, forge.alloc("ingen")); + add("jackServer", "jack-server", 's', "JACK server name", GLOBAL, forge.String, forge.alloc("")); + add("uuid", "uuid", 'u', "JACK session UUID", GLOBAL, forge.String, Atom()); + add("load", "load", 'l', "Load graph", SESSION, forge.String, Atom()); + add("serverLoad", "server-load", 'i', "Load graph (server side)", SESSION, forge.String, Atom()); + add("save", "save", 'o', "Save graph", SESSION, forge.String, Atom()); + add("execute", "execute", 'x', "File of commands to execute", SESSION, forge.String, Atom()); + add("path", "path", 'L', "Target path for loaded graph", SESSION, forge.String, Atom()); + add("queueSize", "queue-size", 'q', "Event queue size", GLOBAL, forge.Int, forge.make(4096)); + add("flushLog", "flush-log", 'f', "Flush logs after every entry", GLOBAL, forge.Bool, forge.make(false)); + add("dump", "dump", 'd', "Print debug output", SESSION, forge.Bool, forge.make(false)); + add("trace", "trace", 't', "Show LV2 plugin trace messages", SESSION, forge.Bool, forge.make(false)); + add("threads", "threads", 'p', "Number of processing threads", GLOBAL, forge.Int, forge.make(int32_t(std::max(std::thread::hardware_concurrency(), 1U)))); + add("humanNames", "human-names", 0, "Show human names in GUI", GUI, forge.Bool, forge.make(true)); + add("portLabels", "port-labels", 0, "Show port labels in GUI", GUI, forge.Bool, forge.make(true)); + add("graphDirectory", "graph-directory", 0, "Default directory for opening graphs", GUI, forge.String, Atom()); +} + +Configuration& +Configuration::add(const std::string& key, + const std::string& name, + char letter, + const std::string& desc, + Scope scope, + const LV2_URID type, + const Atom& value) +{ + assert(value.type() == type || value.type() == 0); + _max_name_length = std::max(_max_name_length, name.length()); + _options.emplace(name, Option{key, name, letter, desc, scope, type, value}); + if (!key.empty()) { + _keys.emplace(key, name); + } + if (letter != '\0') { + _short_names.emplace(letter, name); + } + return *this; +} + +std::string +Configuration::variable_string(LV2_URID type) const +{ + if (type == _forge.String) { + return "=STRING"; + } else if (type == _forge.Int) { + return "=INT"; + } + return ""; +} + +void +Configuration::print_usage(const std::string& program, std::ostream& os) +{ + os << "Usage: " << program << " [OPTION]... [GRAPH]" << std::endl; + os << _shortdesc << std::endl << std::endl; + os << _desc << std::endl << std::endl; + os << "Options:" << std::endl; + for (const auto& o : _options) { + const Option& option = o.second; + os << " "; + if (option.letter != '\0') { + os << "-" << option.letter << ", "; + } else { + os << " "; + } + os.width(_max_name_length + 11); + os << std::left; + os << (std::string("--") + o.first + variable_string(option.type)); + os << option.desc << std::endl; + } +} + +int +Configuration::set_value_from_string(Configuration::Option& option, + const std::string& value) +{ + if (option.type == _forge.Int) { + char* endptr = nullptr; + int intval = static_cast<int>(strtol(value.c_str(), &endptr, 10)); + if (endptr && *endptr == '\0') { + option.value = _forge.make(intval); + } else { + throw OptionError( + (fmt("Option `%1%' has non-integer value `%2%'") + % option.name % value).str()); + } + } else if (option.type == _forge.String) { + option.value = _forge.alloc(value.c_str()); + assert(option.value.type() == _forge.String); + } else if (option.type == _forge.Bool) { + option.value = _forge.make(bool(!strcmp(value.c_str(), "true"))); + assert(option.value.type() == _forge.Bool); + } else { + throw OptionError( + (fmt("Bad option type `%1%'") % option.name).str()); + } + return EXIT_SUCCESS; +} + +/** Parse command line arguments. */ +void +Configuration::parse(int argc, char** argv) +{ + for (int i = 1; i < argc; ++i) { + if (argv[i][0] != '-' || !strcmp(argv[i], "-")) { + // File argument + const Options::iterator o = _options.find("load"); + if (!o->second.value.is_valid()) { + _options.find("load")->second.value = _forge.alloc(argv[i]); + } else { + throw OptionError("Multiple graphs specified"); + } + } else if (argv[i][1] == '-') { + // Long option + std::string name = std::string(argv[i]).substr(2); + const char* equals = strchr(argv[i], '='); + if (equals) { + name = name.substr(0, name.find('=')); + } + + const Options::iterator o = _options.find(name); + if (o == _options.end()) { + throw OptionError( + (fmt("Unrecognized option `%1%'") % name).str()); + } else if (o->second.type == _forge.Bool) { // --flag + o->second.value = _forge.make(true); + } else if (equals) { // --opt=val + set_value_from_string(o->second, equals + 1); + } else if (++i < argc) { // --opt val + set_value_from_string(o->second, argv[i]); + } else { + throw OptionError( + (fmt("Missing value for `%1%'") % name).str()); + } + } else { + // Short option + const size_t len = strlen(argv[i]); + for (size_t j = 1; j < len; ++j) { + const char letter = argv[i][j]; + const ShortNames::iterator n = _short_names.find(letter); + if (n == _short_names.end()) { + throw OptionError( + (fmt("Unrecognized option `%1%'") % letter).str()); + } + + const Options::iterator o = _options.find(n->second); + if (j < len - 1) { // Non-final POSIX style flag + if (o->second.type != _forge.Bool) { + throw OptionError( + (fmt("Missing value for `%1%'") % letter).str()); + } + o->second.value = _forge.make(true); + } else if (o->second.type == _forge.Bool) { // -f + o->second.value = _forge.make(true); + } else if (++i < argc) { // -v val + set_value_from_string(o->second, argv[i]); + } else { + throw OptionError( + (fmt("Missing value for `%1%'") % letter).str()); + } + } + } + } +} + +bool +Configuration::load(const FilePath& path) +{ + if (!filesystem::exists(path)) { + return false; + } + + SerdNode node = serd_node_new_file_uri( + (const uint8_t*)path.c_str(), nullptr, nullptr, true); + const std::string uri((const char*)node.buf); + + Sord::World world; + Sord::Model model(world, uri, SORD_SPO, false); + SerdEnv* env = serd_env_new(&node); + model.load_file(env, SERD_TURTLE, uri, uri); + + Sord::Node nodemm(world, Sord::Node::URI, (const char*)node.buf); + Sord::Node nil; + for (Sord::Iter i = model.find(nodemm, nil, nil); !i.end(); ++i) { + const Sord::Node& pred = i.get_predicate(); + const Sord::Node& obj = i.get_object(); + if (pred.to_string().substr(0, sizeof(INGEN_NS) - 1) == INGEN_NS) { + const std::string key = pred.to_string().substr(sizeof(INGEN_NS) - 1); + const Keys::iterator k = _keys.find(key); + if (k != _keys.end() && obj.type() == Sord::Node::LITERAL) { + set_value_from_string(_options.find(k->second)->second, + obj.to_string()); + } + } + } + + serd_node_free(&node); + serd_env_free(env); + return true; +} + +FilePath +Configuration::save(URIMap& uri_map, + const std::string& app, + const FilePath& filename, + unsigned scopes) +{ + // Save to file if it is absolute, otherwise save to user config dir + FilePath path = filename; + if (!path.is_absolute()) { + path = FilePath(user_config_dir()) / app / filename; + } + + // Create parent directories if necessary + const FilePath dir = path.parent_path(); + if (!filesystem::create_directories(dir)) { + throw FileError((fmt("Error creating directory %1% (%2%)") + % dir % strerror(errno)).str()); + } + + // Attempt to open file for writing + FILE* file = fopen(path.c_str(), "w"); + if (!file) { + throw FileError((fmt("Failed to open file %1% (%2%)") + % path % strerror(errno)).str()); + } + + // Use the file's URI as the base URI + SerdURI base_uri; + SerdNode base = serd_node_new_file_uri( + (const uint8_t*)path.c_str(), nullptr, &base_uri, true); + + // Create environment with ingen prefix + SerdEnv* env = serd_env_new(&base); + serd_env_set_prefix_from_strings( + env, (const uint8_t*)"ingen", (const uint8_t*)INGEN_NS); + + // Create Turtle writer + SerdWriter* writer = serd_writer_new( + SERD_TURTLE, + (SerdStyle)(SERD_STYLE_RESOLVED|SERD_STYLE_ABBREVIATED), + env, + &base_uri, + serd_file_sink, + file); + + // Write a prefix directive for each prefix in the environment + serd_env_foreach(env, (SerdPrefixSink)serd_writer_set_prefix, writer); + + // Create an atom serialiser and connect it to the Turtle writer + Sratom* sratom = sratom_new(&uri_map.urid_map_feature()->urid_map); + sratom_set_pretty_numbers(sratom, true); + sratom_set_sink(sratom, (const char*)base.buf, + (SerdStatementSink)serd_writer_write_statement, nullptr, + writer); + + // Write a statement for each valid option + for (auto o : _options) { + const Atom& value = o.second.value; + if (!(o.second.scope & scopes) || + o.second.key.empty() || + !value.is_valid()) { + continue; + } + + const std::string key(std::string("ingen:") + o.second.key); + SerdNode pred = serd_node_from_string( + SERD_CURIE, (const uint8_t*)key.c_str()); + sratom_write(sratom, &uri_map.urid_unmap_feature()->urid_unmap, 0, + &base, &pred, value.type(), value.size(), value.get_body()); + } + + sratom_free(sratom); + serd_writer_free(writer); + serd_env_free(env); + serd_node_free(&base); + fclose(file); + + return path; +} + +std::list<FilePath> +Configuration::load_default(const std::string& app, const FilePath& filename) +{ + std::list<FilePath> loaded; + + const std::vector<FilePath> dirs = system_config_dirs(); + for (const auto& d : dirs) { + const FilePath path = d / app / filename; + if (load(path)) { + loaded.push_back(path); + } + } + + const FilePath path = user_config_dir() / app / filename; + if (load(path)) { + loaded.push_back(path); + } + + return loaded; +} + +const Atom& +Configuration::option(const std::string& long_name) const +{ + static const Atom nil; + auto o = _options.find(long_name); + if (o == _options.end()) { + return nil; + } else { + return o->second.value; + } +} + +bool +Configuration::set(const std::string& long_name, const Atom& value) +{ + auto o = _options.find(long_name); + if (o != _options.end()) { + o->second.value = value; + return true; + } + return false; +} + +} // namespace Ingen diff --git a/src/FilePath.cpp b/src/FilePath.cpp new file mode 100644 index 00000000..557fabc9 --- /dev/null +++ b/src/FilePath.cpp @@ -0,0 +1,242 @@ +/* + This file is part of Ingen. + Copyright 2018 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/FilePath.hpp" + +namespace Ingen { + +template <typename Char> +static bool +is_sep(const Char chr) +{ +#ifdef USE_WINDOWS_FILE_PATHS + return chr == L'/' || chr == preferred_separator; +#else + return chr == '/'; +#endif +} + +FilePath& +FilePath::operator=(FilePath&& path) noexcept +{ + _str = std::move(path._str); + path.clear(); + return *this; +} + +FilePath& +FilePath::operator=(string_type&& str) +{ + return *this = FilePath(std::move(str)); +} + +FilePath& +FilePath::operator/=(const FilePath& path) +{ + const FilePath::string_type str = path.string(); + if (!_str.empty() && !is_sep(_str.back()) && !str.empty() && + !is_sep(str.front())) { + _str += preferred_separator; + } + + _str += str; + return *this; +} + +FilePath& +FilePath::operator+=(const FilePath& path) +{ + return operator+=(path.native()); +} + +FilePath& +FilePath::operator+=(const string_type& str) +{ + _str += str; + return *this; +} + +FilePath& +FilePath::operator+=(const value_type* str) +{ + _str += str; + return *this; +} + +FilePath& +FilePath::operator+=(value_type chr) +{ + _str += chr; + return *this; +} + +FilePath& +FilePath::operator+=(boost::basic_string_view<value_type> sv) +{ + _str.append(sv.data(), sv.size()); + return *this; +} + +FilePath +FilePath::root_name() const +{ +#ifdef USE_WINDOWS_FILE_PATHS + if (_str.length() >= 2 && _str[0] >= 'A' && _str[0] <= 'Z' && + _str[1] == ':') { + return FilePath(_str.substr(0, 2)); + } +#endif + + return FilePath(); +} + +FilePath +FilePath::root_directory() const +{ +#ifdef USE_WINDOWS_FILE_PATHS + const auto name = root_name().string(); + return name.empty() ? Path() : Path(name + preferred_separator); +#endif + + return _str[0] == '/' ? FilePath("/") : FilePath(); +} + +FilePath +FilePath::root_path() const +{ +#ifdef USE_WINDOWS_FILE_PATHS + const auto name = root_name(); + return name.empty() ? FilePath() : name / root_directory(); +#endif + return root_directory(); +} + +FilePath +FilePath::relative_path() const +{ + const auto root = root_path(); + return root.empty() ? FilePath() + : FilePath(_str.substr(root.string().length())); +} + +FilePath +FilePath::parent_path() const +{ + if (empty() || *this == root_path()) { + return *this; + } + + const auto first_sep = find_first_sep(); + const auto last_sep = find_last_sep(); + return ((last_sep == std::string::npos || last_sep == first_sep) + ? root_path() + : FilePath(_str.substr(0, last_sep))); +} + +FilePath +FilePath::filename() const +{ + return ((empty() || *this == root_path()) + ? FilePath() + : FilePath(_str.substr(find_last_sep() + 1))); +} + +FilePath +FilePath::stem() const +{ + const auto name = filename(); + const auto dot = name.string().find('.'); + return ((dot == std::string::npos) ? name + : FilePath(name.string().substr(0, dot))); +} + +FilePath +FilePath::extension() const +{ + const auto name = filename().string(); + const auto dot = name.find('.'); + return ((dot == std::string::npos) ? FilePath() + : FilePath(name.substr(dot, dot))); +} + +bool +FilePath::is_absolute() const +{ +#ifdef USE_WINDOWS_FILE_PATHS + return !root_name().empty(); +#else + return !root_directory().empty(); +#endif +} + +std::size_t +FilePath::find_first_sep() const +{ + const auto i = std::find_if(_str.begin(), _str.end(), is_sep<value_type>); + return i == _str.end() ? std::string::npos : (i - _str.begin()); +} + +std::size_t +FilePath::find_last_sep() const +{ + const auto i = std::find_if(_str.rbegin(), _str.rend(), is_sep<value_type>); + return (i == _str.rend() ? std::string::npos + : (_str.length() - 1 - (i - _str.rbegin()))); +} + +bool +operator==(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return lhs.string() == rhs.string(); +} + +bool +operator!=(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return !(lhs == rhs); +} + +bool +operator<(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return lhs.string().compare(rhs.string()) < 0; +} + +bool +operator<=(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return !(rhs < lhs); +} + +bool +operator>(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return rhs < lhs; +} + +bool +operator>=(const FilePath& lhs, const FilePath& rhs) noexcept +{ + return !(lhs < rhs); +} + +FilePath +operator/(const FilePath& lhs, const FilePath& rhs) +{ + return FilePath(lhs) /= rhs; +} + +} // namespace Ingen diff --git a/src/Forge.cpp b/src/Forge.cpp new file mode 100644 index 00000000..688d994d --- /dev/null +++ b/src/Forge.cpp @@ -0,0 +1,69 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <sstream> + +#include "ingen/Forge.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" + +namespace Ingen { + +Forge::Forge(URIMap& map) + : _map(map) +{ + lv2_atom_forge_init(this, &map.urid_map_feature()->urid_map); +} + +Atom +Forge::make_urid(const Ingen::URI& u) +{ + const LV2_URID urid = _map.map_uri(u.string()); + return Atom(sizeof(int32_t), URID, &urid); +} + +std::string +Forge::str(const Atom& atom, bool quoted) +{ + std::ostringstream ss; + if (atom.type() == Int) { + ss << atom.get<int32_t>(); + } else if (atom.type() == Float) { + ss << atom.get<float>(); + } else if (atom.type() == Bool) { + ss << (atom.get<int32_t>() ? "true" : "false"); + } else if (atom.type() == URI) { + ss << (quoted ? "<" : "") + << atom.ptr<const char>() + << (quoted ? ">" : ""); + } else if (atom.type() == URID) { + ss << (quoted ? "<" : "") + << _map.unmap_uri(atom.get<int32_t>()) + << (quoted ? ">" : ""); + } else if (atom.type() == Path) { + ss << (quoted ? "<" : "") + << atom.ptr<const char>() + << (quoted ? ">" : ""); + } else if (atom.type() == String) { + ss << (quoted ? "\"" : "") + << atom.ptr<const char>() + << (quoted ? "\"" : ""); + } + return ss.str(); +} + +} // namespace Ingen diff --git a/src/LV2Features.cpp b/src/LV2Features.cpp new file mode 100644 index 00000000..356df42c --- /dev/null +++ b/src/LV2Features.cpp @@ -0,0 +1,83 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> + +#include "ingen/LV2Features.hpp" + +namespace Ingen { + +void +LV2Features::Feature::free_feature(LV2_Feature* feature) +{ + free(feature->data); + free(feature); +} + +LV2Features::LV2Features() +{ +} + +void +LV2Features::add_feature(SPtr<Feature> feature) +{ + _features.push_back(feature); +} + +LV2Features::FeatureArray::FeatureArray(FeatureVector& features) + : _features(features) +{ + _array = (LV2_Feature**)malloc(sizeof(LV2_Feature*) * (features.size() + 1)); + _array[features.size()] = nullptr; + for (size_t i = 0; i < features.size(); ++i) { + _array[i] = features[i].get(); + } +} + +LV2Features::FeatureArray::~FeatureArray() +{ + free(_array); +} + +bool +LV2Features::is_supported(const std::string& uri) const +{ + if (uri == "http://lv2plug.in/ns/lv2core#isLive") { + return true; + } + + for (const auto& f : _features) { + if (f->uri() == uri) { + return true; + } + } + return false; +} + +SPtr<LV2Features::FeatureArray> +LV2Features::lv2_features(World* world, Node* node) const +{ + FeatureArray::FeatureVector vec; + for (const auto& f : _features) { + SPtr<LV2_Feature> fptr = f->feature(world, node); + if (fptr) { + vec.push_back(fptr); + } + } + return SPtr<FeatureArray>(new FeatureArray(vec)); +} + +} // namespace Ingen diff --git a/src/Library.cpp b/src/Library.cpp new file mode 100644 index 00000000..148b27d0 --- /dev/null +++ b/src/Library.cpp @@ -0,0 +1,56 @@ +/* + This file is part of Ingen. + Copyright 2018 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Library.hpp" + +#ifdef _WIN32 +# include <windows.h> +# define dlopen(path, flags) LoadLibrary(path) +# define dlclose(lib) FreeLibrary((HMODULE)lib) +# define dlerror() "unknown error" +#else +# include <dlfcn.h> +#endif + +namespace Ingen { + +Library::Library(const FilePath& path) : _lib(dlopen(path.c_str(), RTLD_NOW)) +{} + +Library::~Library() +{ + dlclose(_lib); +} + +Library::VoidFuncPtr +Library::get_function(const char* name) +{ +#ifdef _WIN32 + return (VoidFuncPtr)GetProcAddress((HMODULE)_lib, name); +#else + typedef VoidFuncPtr (*VoidFuncGetter)(void*, const char*); + VoidFuncGetter dlfunc = (VoidFuncGetter)dlsym; + return dlfunc(_lib, name); +#endif +} + +const char* +Library::get_last_error() +{ + return dlerror(); +} + +} // namespace Ingen diff --git a/src/Log.cpp b/src/Log.cpp new file mode 100644 index 00000000..6145bcd1 --- /dev/null +++ b/src/Log.cpp @@ -0,0 +1,162 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdio> + +#include "ingen/Log.hpp" +#include "ingen/Node.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/ColorContext.hpp" + +namespace Ingen { + +Log::Log(LV2_Log_Log* log, URIs& uris) + : _log(log) + , _uris(uris) + , _flush(false) + , _trace(false) +{} + +void +Log::rt_error(const char* msg) +{ +#ifndef NDEBUG + va_list args; + vtprintf(_uris.log_Error, msg, args); +#endif +} + +void +Log::error(const std::string& msg) +{ + va_list args; + vtprintf(_uris.log_Error, msg.c_str(), args); +} + +void +Log::warn(const std::string& msg) +{ + va_list args; + vtprintf(_uris.log_Warning, msg.c_str(), args); +} + +void +Log::info(const std::string& msg) +{ + va_list args; + vtprintf(_uris.log_Note, msg.c_str(), args); +} + +void +Log::trace(const std::string& msg) +{ + va_list args; + vtprintf(_uris.log_Trace, msg.c_str(), args); +} + +void +Log::print(FILE* stream, const std::string& msg) +{ + fprintf(stream, "%s", msg.c_str()); + if (_flush) { + fflush(stdout); + } +} + +int +Log::vtprintf(LV2_URID type, const char* fmt, va_list args) +{ + int ret = 0; + if (type == _uris.log_Trace && !_trace) { + return 0; + } else if (_sink) { + _sink(type, fmt, args); + } + + if (_log) { + ret = _log->vprintf(_log->handle, type, fmt, args); + } else if (type == _uris.log_Error) { + ColorContext ctx(stderr, ColorContext::Color::RED); + ret = vfprintf(stderr, fmt, args); + } else if (type == _uris.log_Warning) { + ColorContext ctx(stderr, ColorContext::Color::YELLOW); + ret = vfprintf(stderr, fmt, args); + } else if (type == _uris.log_Note) { + ColorContext ctx(stderr, ColorContext::Color::GREEN); + ret = vfprintf(stdout, fmt, args); + } else if (_trace && type == _uris.log_Trace) { + ColorContext ctx(stderr, ColorContext::Color::GREEN); + ret = vfprintf(stderr, fmt, args); + } else { + fprintf(stderr, "Unknown log type %d\n", type); + return 0; + } + if (_flush) { + fflush(stdout); + } + return ret; +} + +static int +log_vprintf(LV2_Log_Handle handle, LV2_URID type, const char* fmt, va_list args) +{ + Log::Feature::Handle* f = (Log::Feature::Handle*)handle; + va_list noargs; + + int ret = f->log->vtprintf(type, f->node->path().c_str(), noargs); + ret += f->log->vtprintf(type, ": ", noargs); + ret += f->log->vtprintf(type, fmt, args); + + return ret; +} + +static int +log_printf(LV2_Log_Handle handle, LV2_URID type, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + const int ret = log_vprintf(handle, type, fmt, args); + va_end(args); + + return ret; +} + +static void +free_log_feature(LV2_Feature* feature) { + LV2_Log_Log* lv2_log = (LV2_Log_Log*)feature->data; + free(lv2_log->handle); + free(feature); +} + +SPtr<LV2_Feature> +Log::Feature::feature(World* world, Node* block) +{ + Handle* handle = (Handle*)calloc(1, sizeof(Handle)); + handle->lv2_log.handle = handle; + handle->lv2_log.printf = log_printf; + handle->lv2_log.vprintf = log_vprintf; + handle->log = &world->log(); + handle->node = block; + + LV2_Feature* f = (LV2_Feature*)malloc(sizeof(LV2_Feature)); + f->URI = LV2_LOG__log; + f->data = &handle->lv2_log; + + return SPtr<LV2_Feature>(f, &free_log_feature); +} + +} // namespace Ingen diff --git a/src/Parser.cpp b/src/Parser.cpp new file mode 100644 index 00000000..dace07ed --- /dev/null +++ b/src/Parser.cpp @@ -0,0 +1,713 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <map> +#include <set> +#include <string> +#include <utility> + +#include "ingen/Atom.hpp" +#include "ingen/AtomForgeSink.hpp" +#include "ingen/Forge.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Parser.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/filesystem.hpp" +#include "ingen/paths.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "serd/serd.h" +#include "sord/sordmm.hpp" +#include "sratom/sratom.h" + +#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define NS_RDFS "http://www.w3.org/2000/01/rdf-schema#" + +namespace Ingen { + +std::set<Parser::ResourceRecord> +Parser::find_resources(Sord::World& world, + const URI& manifest_uri, + const URI& type_uri) +{ + const Sord::URI base (world, manifest_uri.string()); + const Sord::URI type (world, type_uri.string()); + const Sord::URI rdf_type (world, NS_RDF "type"); + const Sord::URI rdfs_seeAlso(world, NS_RDFS "seeAlso"); + const Sord::Node nil; + + SerdEnv* env = serd_env_new(sord_node_to_serd_node(base.c_obj())); + Sord::Model model(world, manifest_uri.string()); + model.load_file(env, SERD_TURTLE, manifest_uri.string()); + + std::set<ResourceRecord> resources; + for (Sord::Iter i = model.find(nil, rdf_type, type); !i.end(); ++i) { + const Sord::Node resource = i.get_subject(); + const std::string resource_uri = resource.to_c_string(); + std::string file_path = ""; + Sord::Iter f = model.find(resource, rdfs_seeAlso, nil); + if (!f.end()) { + uint8_t* p = serd_file_uri_parse(f.get_object().to_u_string(), nullptr); + file_path = (const char*)p; + serd_free(p); + } + resources.insert(ResourceRecord(resource, file_path)); + } + + serd_env_free(env); + return resources; +} + +static boost::optional<Raul::Path> +get_path(const URI base, const URI uri) +{ + const URI relative = uri.make_relative(base); + const std::string uri_str = "/" + relative.string(); + return Raul::Path::is_valid(uri_str) ? Raul::Path(uri_str) + : boost::optional<Raul::Path>(); +} + +static bool +skip_property(Ingen::URIs& uris, const Sord::Node& predicate) +{ + return (predicate == INGEN__file || + predicate == uris.ingen_arc || + predicate == uris.ingen_block || + predicate == uris.lv2_port); +} + +static Properties +get_properties(Ingen::World* world, + Sord::Model& model, + const Sord::Node& subject, + Resource::Graph ctx) +{ + LV2_URID_Map* map = &world->uri_map().urid_map_feature()->urid_map; + Sratom* sratom = sratom_new(map); + + LV2_Atom_Forge forge; + lv2_atom_forge_init(&forge, map); + + AtomForgeSink out(&forge); + + const Sord::Node nil; + Properties props; + for (Sord::Iter i = model.find(subject, nil, nil); !i.end(); ++i) { + if (!skip_property(world->uris(), i.get_predicate())) { + out.clear(); + sratom_read(sratom, &forge, world->rdf_world()->c_obj(), + model.c_obj(), i.get_object().c_obj()); + const LV2_Atom* atom = out.atom(); + Atom atomm; + atomm = world->forge().alloc( + atom->size, atom->type, LV2_ATOM_BODY_CONST(atom)); + props.emplace(i.get_predicate(), Property(atomm, ctx)); + } + } + + sratom_free(sratom); + return props; +} + +typedef std::pair<Raul::Path, Properties> PortRecord; + +static boost::optional<PortRecord> +get_port(Ingen::World* world, + Sord::Model& model, + const Sord::Node& subject, + Resource::Graph ctx, + const Raul::Path& parent, + uint32_t* index) +{ + const URIs& uris = world->uris(); + + // Get all properties + Properties props = get_properties(world, model, subject, ctx); + + // Get index if requested (for Graphs) + if (index) { + Properties::const_iterator i = props.find(uris.lv2_index); + if (i == props.end() + || i->second.type() != world->forge().Int + || i->second.get<int32_t>() < 0) { + world->log().error(fmt("Port %1% has no valid index\n") % subject); + return boost::optional<PortRecord>(); + } + *index = i->second.get<int32_t>(); + } + + // Get symbol + Properties::const_iterator s = props.find(uris.lv2_symbol); + std::string sym; + if (s != props.end() && s->second.type() == world->forge().String) { + sym = s->second.ptr<char>(); + } else { + const std::string subject_str = subject.to_string(); + const size_t last_slash = subject_str.find_last_of("/"); + + sym = ((last_slash == std::string::npos) + ? subject_str + : subject_str.substr(last_slash + 1)); + } + + if (!Raul::Symbol::is_valid(sym)) { + world->log().error(fmt("Port %1% has invalid symbol `%2%'\n") + % subject % sym); + return boost::optional<PortRecord>(); + } + + const Raul::Symbol port_sym(sym); + const Raul::Path port_path(parent.child(port_sym)); + + props.erase(uris.lv2_symbol); // Don't set symbol property in engine + return make_pair(port_path, props); +} + +static boost::optional<Raul::Path> +parse( + World* world, + Interface* target, + Sord::Model& model, + const URI& base_uri, + Sord::Node& subject, + boost::optional<Raul::Path> parent = boost::optional<Raul::Path>(), + boost::optional<Raul::Symbol> symbol = boost::optional<Raul::Symbol>(), + boost::optional<Properties> data = boost::optional<Properties>()); + +static boost::optional<Raul::Path> +parse_graph( + World* world, + Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + Resource::Graph ctx, + boost::optional<Raul::Path> parent = boost::optional<Raul::Path>(), + boost::optional<Raul::Symbol> symbol = boost::optional<Raul::Symbol>(), + boost::optional<Properties> data = boost::optional<Properties>()); + +static boost::optional<Raul::Path> +parse_block( + World* world, + Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& path, + boost::optional<Properties> data = boost::optional<Properties>()); + +static bool +parse_properties( + World* world, + Interface* target, + Sord::Model& model, + const Sord::Node& subject, + Resource::Graph ctx, + const URI& uri, + boost::optional<Properties> data = boost::optional<Properties>()); + +static bool +parse_arcs( + World* world, + Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& graph); + +static boost::optional<Raul::Path> +parse_block(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& path, + boost::optional<Properties> data) +{ + const URIs& uris = world->uris(); + + // Try lv2:prototype and old ingen:prototype for backwards compatibility + const Sord::URI prototype_predicates[] = { + Sord::URI(*world->rdf_world(), uris.lv2_prototype), + Sord::URI(*world->rdf_world(), uris.ingen_prototype) + }; + + // Get prototype + Sord::Node prototype; + for (const Sord::URI& pred : prototype_predicates) { + prototype = model.get(subject, pred, Sord::Node()); + if (prototype.is_valid()) { + break; + } + } + + if (!prototype.is_valid()) { + world->log().error( + fmt("Block %1% (%2%) missing mandatory lv2:prototype\n") % + subject % path); + return boost::optional<Raul::Path>(); + } + + const uint8_t* type_uri = (const uint8_t*)prototype.to_c_string(); + if (!serd_uri_string_has_scheme(type_uri) || + !strncmp((const char*)type_uri, "file:", 5)) { + // Prototype is a file, subgraph + SerdURI base_uri_parts; + serd_uri_parse((const uint8_t*)base_uri.c_str(), &base_uri_parts); + + SerdURI ignored; + SerdNode sub_uri = serd_node_new_uri_from_string( + type_uri, + &base_uri_parts, + &ignored); + + const std::string sub_uri_str = (const char*)sub_uri.buf; + const std::string sub_file = sub_uri_str + "/main.ttl"; + + const SerdNode sub_base = serd_node_from_string( + SERD_URI, (const uint8_t*)sub_file.c_str()); + + Sord::Model sub_model(*world->rdf_world(), sub_file); + SerdEnv* env = serd_env_new(&sub_base); + sub_model.load_file(env, SERD_TURTLE, sub_file); + serd_env_free(env); + + Sord::URI sub_node(*world->rdf_world(), sub_file); + parse_graph(world, target, sub_model, sub_base, + sub_node, Resource::Graph::INTERNAL, + path.parent(), Raul::Symbol(path.symbol())); + + parse_graph(world, target, model, base_uri, + subject, Resource::Graph::EXTERNAL, + path.parent(), Raul::Symbol(path.symbol())); + } else { + // Prototype is non-file URI, plugin + Properties props = get_properties( + world, model, subject, Resource::Graph::DEFAULT); + props.emplace(uris.rdf_type, uris.forge.make_urid(uris.ingen_Block)); + target->put(path_to_uri(path), props); + } + return path; +} + +static boost::optional<Raul::Path> +parse_graph(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + Resource::Graph ctx, + boost::optional<Raul::Path> parent, + boost::optional<Raul::Symbol> symbol, + boost::optional<Properties> data) +{ + const URIs& uris = world->uris(); + + const Sord::URI ingen_block(*world->rdf_world(), uris.ingen_block); + const Sord::URI lv2_port(*world->rdf_world(), LV2_CORE__port); + + const Sord::Node& graph = subject; + const Sord::Node nil; + + // Build graph path and symbol + Raul::Path graph_path; + if (parent && symbol) { + graph_path = parent->child(*symbol); + } else if (parent) { + graph_path = *parent; + } else { + graph_path = Raul::Path("/"); + } + + if (!symbol) { + symbol = Raul::Symbol("_"); + } + + // Create graph + Properties props = get_properties(world, model, subject, ctx); + target->put(path_to_uri(graph_path), props, ctx); + + // For each port on this graph + typedef std::map<uint32_t, PortRecord> PortRecords; + PortRecords ports; + for (Sord::Iter p = model.find(graph, lv2_port, nil); !p.end(); ++p) { + Sord::Node port = p.get_object(); + + // Get all properties + uint32_t index = 0; + boost::optional<PortRecord> port_record = get_port( + world, model, port, ctx, graph_path, &index); + if (!port_record) { + world->log().error(fmt("Invalid port %1%\n") % port); + return boost::optional<Raul::Path>(); + } + + // Store port information in ports map + if (ports.find(index) == ports.end()) { + ports[index] = *port_record; + } else { + world->log().error(fmt("Ignored port %1% with duplicate index %2%\n") + % port % index); + } + } + + // Create ports in order by index + for (const auto& p : ports) { + target->put(path_to_uri(p.second.first), + p.second.second, + ctx); + } + + if (ctx != Resource::Graph::INTERNAL) { + return graph_path; // Not parsing graph internals, finished now + } + + // For each block in this graph + for (Sord::Iter n = model.find(subject, ingen_block, nil); !n.end(); ++n) { + Sord::Node node = n.get_object(); + URI node_uri = node; + assert(!node_uri.path().empty() && node_uri.path() != "/"); + const Raul::Path block_path = graph_path.child( + Raul::Symbol(FilePath(node_uri.path()).stem().string())); + + // Parse and create block + parse_block(world, target, model, base_uri, node, block_path, + boost::optional<Properties>()); + + // For each port on this block + for (Sord::Iter p = model.find(node, lv2_port, nil); !p.end(); ++p) { + Sord::Node port = p.get_object(); + + Resource::Graph subctx = Resource::Graph::DEFAULT; + if (!model.find(node, + Sord::URI(*world->rdf_world(), uris.rdf_type), + Sord::URI(*world->rdf_world(), uris.ingen_Graph)).end()) { + subctx = Resource::Graph::EXTERNAL; + } + + // Get all properties + boost::optional<PortRecord> port_record = get_port( + world, model, port, subctx, block_path, nullptr); + if (!port_record) { + world->log().error(fmt("Invalid port %1%\n") % port); + return boost::optional<Raul::Path>(); + } + + // Create port and/or set all port properties + target->put(path_to_uri(port_record->first), + port_record->second, + subctx); + } + } + + // Now that all ports and blocks exist, create arcs inside graph + parse_arcs(world, target, model, base_uri, subject, graph_path); + + return graph_path; +} + +static bool +parse_arc(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& graph) +{ + const URIs& uris = world->uris(); + + const Sord::URI ingen_tail(*world->rdf_world(), uris.ingen_tail); + const Sord::URI ingen_head(*world->rdf_world(), uris.ingen_head); + const Sord::Node nil; + + Sord::Iter t = model.find(subject, ingen_tail, nil); + Sord::Iter h = model.find(subject, ingen_head, nil); + + if (t.end()) { + world->log().error("Arc has no tail\n"); + return false; + } else if (h.end()) { + world->log().error("Arc has no head\n"); + return false; + } + + const boost::optional<Raul::Path> tail_path = get_path( + base_uri, t.get_object()); + if (!tail_path) { + world->log().error("Arc tail has invalid URI\n"); + return false; + } + + const boost::optional<Raul::Path> head_path = get_path( + base_uri, h.get_object()); + if (!head_path) { + world->log().error("Arc head has invalid URI\n"); + return false; + } + + if (!(++t).end()) { + world->log().error("Arc has multiple tails\n"); + return false; + } else if (!(++h).end()) { + world->log().error("Arc has multiple heads\n"); + return false; + } + + target->connect(graph.child(*tail_path), graph.child(*head_path)); + + return true; +} + +static bool +parse_arcs(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + const Sord::Node& subject, + const Raul::Path& graph) +{ + const Sord::URI ingen_arc(*world->rdf_world(), world->uris().ingen_arc); + const Sord::Node nil; + + for (Sord::Iter i = model.find(subject, ingen_arc, nil); !i.end(); ++i) { + parse_arc(world, target, model, base_uri, i.get_object(), graph); + } + + return true; +} + +static bool +parse_properties(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const Sord::Node& subject, + Resource::Graph ctx, + const URI& uri, + boost::optional<Properties> data) +{ + Properties properties = get_properties(world, model, subject, ctx); + + target->put(uri, properties, ctx); + + // Set passed properties last to override any loaded values + if (data) { + target->put(uri, data.get(), ctx); + } + + return true; +} + +static boost::optional<Raul::Path> +parse(Ingen::World* world, + Ingen::Interface* target, + Sord::Model& model, + const URI& base_uri, + Sord::Node& subject, + boost::optional<Raul::Path> parent, + boost::optional<Raul::Symbol> symbol, + boost::optional<Properties> data) +{ + const URIs& uris = world->uris(); + + const Sord::URI graph_class (*world->rdf_world(), uris.ingen_Graph); + const Sord::URI block_class (*world->rdf_world(), uris.ingen_Block); + const Sord::URI arc_class (*world->rdf_world(), uris.ingen_Arc); + const Sord::URI internal_class(*world->rdf_world(), uris.ingen_Internal); + const Sord::URI in_port_class (*world->rdf_world(), LV2_CORE__InputPort); + const Sord::URI out_port_class(*world->rdf_world(), LV2_CORE__OutputPort); + const Sord::URI lv2_class (*world->rdf_world(), LV2_CORE__Plugin); + const Sord::URI rdf_type (*world->rdf_world(), uris.rdf_type); + const Sord::Node nil; + + // Parse explicit subject graph + if (subject.is_valid()) { + return parse_graph(world, target, model, base_uri, + subject, Resource::Graph::INTERNAL, + parent, symbol, data); + } + + // Get all subjects and their types (?subject a ?type) + typedef std::map< Sord::Node, std::set<Sord::Node> > Subjects; + Subjects subjects; + for (Sord::Iter i = model.find(subject, rdf_type, nil); !i.end(); ++i) { + const Sord::Node& subject = i.get_subject(); + const Sord::Node& rdf_class = i.get_object(); + + assert(rdf_class.is_uri()); + auto s = subjects.find(subject); + if (s == subjects.end()) { + std::set<Sord::Node> types; + types.insert(rdf_class); + subjects.emplace(subject, types); + } else { + s->second.insert(rdf_class); + } + } + + // Parse and create each subject + for (const auto& i : subjects) { + const Sord::Node& s = i.first; + const std::set<Sord::Node>& types = i.second; + boost::optional<Raul::Path> ret; + if (types.find(graph_class) != types.end()) { + ret = parse_graph(world, target, model, base_uri, + s, Resource::Graph::INTERNAL, + parent, symbol, data); + } else if (types.find(block_class) != types.end()) { + const Raul::Path rel_path(*get_path(base_uri, s)); + const Raul::Path path = parent ? parent->child(rel_path) : rel_path; + ret = parse_block(world, target, model, base_uri, s, path, data); + } else if (types.find(in_port_class) != types.end() || + types.find(out_port_class) != types.end()) { + const Raul::Path rel_path(*get_path(base_uri, s)); + const Raul::Path path = parent ? parent->child(rel_path) : rel_path; + parse_properties(world, target, model, + s, Resource::Graph::DEFAULT, + path_to_uri(path), data); + ret = path; + } else if (types.find(arc_class) != types.end()) { + Raul::Path parent_path(parent ? parent.get() : Raul::Path("/")); + parse_arc(world, target, model, base_uri, s, parent_path); + } else { + world->log().error("Subject has no known types\n"); + } + } + + return boost::optional<Raul::Path>(); +} + +bool +Parser::parse_file(Ingen::World* world, + Ingen::Interface* target, + const FilePath& path, + boost::optional<Raul::Path> parent, + boost::optional<Raul::Symbol> symbol, + boost::optional<Properties> data) +{ + // Get absolute file path + FilePath file_path = path; + if (!file_path.is_absolute()) { + file_path = filesystem::current_path() / file_path; + } + + // Find file to use as manifest + const bool is_bundle = filesystem::is_directory(file_path); + const FilePath manifest_path = + (is_bundle ? file_path / "manifest.ttl" : file_path); + + URI manifest_uri(manifest_path); + + // Find graphs in manifest + const std::set<ResourceRecord> resources = find_resources( + *world->rdf_world(), manifest_uri, URI(INGEN__Graph)); + + if (resources.empty()) { + world->log().error(fmt("No graphs found in %1%\n") % path); + return false; + } + + /* Choose the graph to load. If this is a manifest, then there should only be + one, but if this is a graph file, subgraphs will be returned as well. + In this case, choose the one with the file URI. */ + URI uri; + for (const ResourceRecord& r : resources) { + if (r.uri == URI(manifest_path)) { + uri = r.uri; + file_path = r.filename; + break; + } + } + + if (uri.empty()) { + // Didn't find a graph with the same URI as the file, use the first + uri = (*resources.begin()).uri; + file_path = (*resources.begin()).filename; + } + + if (file_path.empty()) { + // No seeAlso file, use manifest (probably the graph file itself) + file_path = manifest_path; + } + + // Initialise parsing environment + const URI file_uri = URI(file_path); + const uint8_t* uri_c_str = (const uint8_t*)uri.c_str(); + SerdNode base_node = serd_node_from_string(SERD_URI, uri_c_str); + SerdEnv* env = serd_env_new(&base_node); + + // Load graph into model + Sord::Model model(*world->rdf_world(), uri.string(), SORD_SPO|SORD_PSO, false); + model.load_file(env, SERD_TURTLE, file_uri); + serd_env_free(env); + + world->log().info(fmt("Loading %1% from %2%\n") % uri % file_path); + if (parent) { + world->log().info(fmt("Parent: %1%\n") % parent->c_str()); + } + if (symbol) { + world->log().info(fmt("Symbol: %1%\n") % symbol->c_str()); + } + + Sord::Node subject(*world->rdf_world(), Sord::Node::URI, uri.string()); + boost::optional<Raul::Path> parsed_path + = parse(world, target, model, model.base_uri(), + subject, parent, symbol, data); + + if (parsed_path) { + target->set_property(path_to_uri(*parsed_path), + URI(INGEN__file), + world->forge().alloc_uri(uri.string())); + return true; + } else { + world->log().warn("Document URI lost\n"); + return false; + } +} + +boost::optional<URI> +Parser::parse_string(Ingen::World* world, + Ingen::Interface* target, + const std::string& str, + const URI& base_uri, + boost::optional<Raul::Path> parent, + boost::optional<Raul::Symbol> symbol, + boost::optional<Properties> data) +{ + // Load string into model + Sord::Model model(*world->rdf_world(), base_uri, SORD_SPO|SORD_PSO, false); + + SerdEnv* env = serd_env_new(nullptr); + if (!base_uri.empty()) { + const SerdNode base = serd_node_from_string( + SERD_URI, (const uint8_t*)base_uri.c_str()); + serd_env_set_base_uri(env, &base); + } + model.load_string(env, SERD_TURTLE, str.c_str(), str.length(), base_uri); + + URI actual_base((const char*)serd_env_get_base_uri(env, nullptr)->buf); + serd_env_free(env); + + world->log().info(fmt("Parsing string (base %1%)\n") % base_uri); + + Sord::Node subject; + parse(world, target, model, actual_base, subject, parent, symbol, data); + return actual_base; +} + +} // namespace Ingen diff --git a/src/Resource.cpp b/src/Resource.cpp new file mode 100644 index 00000000..d0261eee --- /dev/null +++ b/src/Resource.cpp @@ -0,0 +1,234 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <utility> + +#include "ingen/Atom.hpp" +#include "ingen/Forge.hpp" +#include "ingen/Resource.hpp" +#include "ingen/URIs.hpp" + +namespace Ingen { + +bool +Resource::add_property(const URI& uri, const Atom& value, Graph ctx) +{ + // Ignore duplicate statements + typedef Properties::const_iterator iterator; + const std::pair<iterator, iterator> range = _properties.equal_range(uri); + for (iterator i = range.first; i != range.second && i != _properties.end(); ++i) { + if (i->second == value && i->second.context() == ctx) { + return false; + } + } + + if (uri != _uris.ingen_activity) { + // Insert new property + const Atom& v = _properties.emplace(uri, Property(value, ctx))->second; + on_property(uri, v); + } else { + // Announce ephemeral activity, but do not store + on_property(uri, value); + } + + return true; +} + +const Atom& +Resource::set_property(const URI& uri, const Atom& value, Resource::Graph ctx) +{ + // Erase existing property in this context + for (auto i = _properties.find(uri); + (i != _properties.end()) && (i->first == uri);) { + auto next = i; + ++next; + if (i->second.context() == ctx) { + const Atom value(i->second); + _properties.erase(i); + on_property_removed(uri, value); + } + i = next; + } + + if (uri != _uris.ingen_activity) { + // Insert new property + const Atom& v = _properties.emplace(uri, Property(value, ctx))->second; + on_property(uri, v); + return v; + } else { + // Announce ephemeral activity, but do not store + on_property(uri, value); + return value; + } +} + +const Atom& +Resource::set_property(const URI& uri, + const URIs::Quark& value, + Resource::Graph ctx) +{ + return set_property(uri, value.urid, ctx); +} + +void +Resource::remove_property(const URI& uri, const Atom& value) +{ + if (_uris.patch_wildcard == value) { + _properties.erase(uri); + } else { + for (auto i = _properties.find(uri); + i != _properties.end() && (i->first == uri); + ++i) { + if (i->second == value) { + _properties.erase(i); + break; + } + } + } + on_property_removed(uri, value); +} + +void +Resource::remove_property(const URI& uri, const URIs::Quark& value) +{ + remove_property(uri, value.urid); + remove_property(uri, value.uri); +} + +bool +Resource::has_property(const URI& uri, const Atom& value) const +{ + return _properties.contains(uri, value); +} + +bool +Resource::has_property(const URI& uri, const URIs::Quark& value) const +{ + Properties::const_iterator i = _properties.find(uri); + for (; (i != _properties.end()) && (i->first == uri); ++i) { + if (value == i->second) { + return true; + } + } + return false; +} + +const Atom& +Resource::set_property(const URI& uri, const Atom& value) const +{ + return const_cast<Resource*>(this)->set_property(uri, value); +} + +const Atom& +Resource::get_property(const URI& uri) const +{ + static const Atom nil; + Properties::const_iterator i = _properties.find(uri); + return (i != _properties.end()) ? i->second : nil; +} + +bool +Resource::type(const URIs& uris, + const Properties& properties, + bool& graph, + bool& block, + bool& port, + bool& is_output) +{ + typedef Properties::const_iterator iterator; + const std::pair<iterator, iterator> types_range = properties.equal_range(uris.rdf_type); + + graph = block = port = is_output = false; + for (iterator i = types_range.first; i != types_range.second; ++i) { + const Atom& atom = i->second; + if (atom.type() != uris.forge.URI && atom.type() != uris.forge.URID) { + continue; // Non-URI type, ignore garbage data + } + + if (uris.ingen_Graph == atom) { + graph = true; + } else if (uris.ingen_Block == atom) { + block = true; + } else if (uris.lv2_InputPort == atom) { + port = true; + is_output = false; + } else if (uris.lv2_OutputPort == atom) { + port = true; + is_output = true; + } + } + + if (graph && block && !port) { // => graph + block = false; + return true; + } else if (port && (graph || block)) { // nonsense + port = false; + return false; + } else if (graph || block || port) { // recognized type + return true; + } else { // unknown + return false; + } +} + +void +Resource::set_properties(const Properties& props) +{ + /* Note a simple loop that calls set_property is inappropriate here since + it will not correctly set multiple properties in p (notably rdf:type) + */ + + // Erase existing properties with matching keys + for (const auto& p : props) { + _properties.erase(p.first); + on_property_removed(p.first, _uris.patch_wildcard.urid); + } + + // Set new properties + add_properties(props); +} + +void +Resource::add_properties(const Properties& props) +{ + for (const auto& p : props) { + add_property(p.first, p.second, p.second.context()); + } +} + +void +Resource::remove_properties(const Properties& props) +{ + for (const auto& p : props) { + remove_property(p.first, p.second); + } +} + +Properties +Resource::properties(Resource::Graph ctx) const +{ + Properties props; + for (const auto& p : _properties) { + if (p.second.context() == ctx) { + props.emplace(p.first, p.second); + } + } + + return props; +} + +} // namespace Ingen diff --git a/src/Serialiser.cpp b/src/Serialiser.cpp new file mode 100644 index 00000000..034ff96b --- /dev/null +++ b/src/Serialiser.cpp @@ -0,0 +1,583 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cerrno> +#include <cstdlib> +#include <cstring> +#include <set> +#include <string> +#include <utility> + +#include "ingen/Arc.hpp" +#include "ingen/FilePath.hpp" +#include "ingen/Forge.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Node.hpp" +#include "ingen/Resource.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/Store.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/filesystem.hpp" +#include "ingen/runtime_paths.hpp" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/extensions/ui/ui.h" +#include "raul/Path.hpp" +#include "sord/sordmm.hpp" +#include "sratom/sratom.h" + +namespace Ingen { + +struct Serialiser::Impl { + explicit Impl(World& world) + : _root_path("/") + , _world(world) + , _model(nullptr) + , _sratom(sratom_new(&_world.uri_map().urid_map_feature()->urid_map)) + {} + + ~Impl() { + sratom_free(_sratom); + } + + enum class Mode { TO_FILE, TO_STRING }; + + void start_to_file(const Raul::Path& root, + const FilePath& filename); + + std::set<const Resource*> serialise_graph(SPtr<const Node> graph, + const Sord::Node& graph_id); + + void serialise_block(SPtr<const Node> block, + const Sord::Node& class_id, + const Sord::Node& block_id); + + void serialise_port(const Node* port, + Resource::Graph context, + const Sord::Node& port_id); + + void serialise_properties(Sord::Node id, + const Properties& props); + + void write_bundle(SPtr<const Node> graph, const URI& uri); + + Sord::Node path_rdf_node(const Raul::Path& path); + + void write_manifest(const FilePath& bundle_path, + SPtr<const Node> graph); + + void write_plugins(const FilePath& bundle_path, + const std::set<const Resource*> plugins); + + void serialise_arc(const Sord::Node& parent, + SPtr<const Arc> arc); + + std::string finish(); + + Raul::Path _root_path; + Mode _mode; + URI _base_uri; + FilePath _basename; + World& _world; + Sord::Model* _model; + Sratom* _sratom; +}; + +Serialiser::Serialiser(World& world) + : me(new Impl(world)) +{} + +Serialiser::~Serialiser() +{ + delete me; +} + +void +Serialiser::Impl::write_manifest(const FilePath& bundle_path, + SPtr<const Node> graph) +{ + const FilePath manifest_path(bundle_path / "manifest.ttl"); + const FilePath binary_path(ingen_module_path("lv2")); + + start_to_file(Raul::Path("/"), manifest_path); + + Sord::World& world = _model->world(); + const URIs& uris = _world.uris(); + + const std::string filename("main.ttl"); + const Sord::URI subject(world, filename, _base_uri); + + _model->add_statement(subject, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.ingen_Graph)); + _model->add_statement(subject, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.lv2_Plugin)); + _model->add_statement(subject, + Sord::URI(world, uris.rdfs_seeAlso), + Sord::URI(world, filename, _base_uri)); + _model->add_statement(subject, + Sord::URI(world, uris.lv2_prototype), + Sord::URI(world, uris.ingen_GraphPrototype)); + + finish(); +} + +void +Serialiser::Impl::write_plugins(const FilePath& bundle_path, + const std::set<const Resource*> plugins) +{ + const FilePath plugins_path(bundle_path / "plugins.ttl"); + + start_to_file(Raul::Path("/"), plugins_path); + + Sord::World& world = _model->world(); + const URIs& uris = _world.uris(); + + for (const auto& p : plugins) { + const Atom& minor = p->get_property(uris.lv2_minorVersion); + const Atom& micro = p->get_property(uris.lv2_microVersion); + + _model->add_statement(Sord::URI(world, p->uri()), + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.lv2_Plugin)); + + if (minor.is_valid() && micro.is_valid()) { + _model->add_statement(Sord::URI(world, p->uri()), + Sord::URI(world, uris.lv2_minorVersion), + Sord::Literal::integer(world, minor.get<int32_t>())); + _model->add_statement(Sord::URI(world, p->uri()), + Sord::URI(world, uris.lv2_microVersion), + Sord::Literal::integer(world, micro.get<int32_t>())); + } + } + + finish(); +} + +void +Serialiser::write_bundle(SPtr<const Node> graph, const URI& uri) +{ + me->write_bundle(graph, uri); +} + +void +Serialiser::Impl::write_bundle(SPtr<const Node> graph, const URI& uri) +{ + FilePath path(uri.path()); + if (filesystem::exists(path) && !filesystem::is_directory(path)) { + path = path.parent_path(); + } + + _world.log().info(fmt("Writing bundle %1%\n") % path); + filesystem::create_directories(path); + + const FilePath main_file = path / "main.ttl"; + const Raul::Path old_root_path = _root_path; + + start_to_file(graph->path(), main_file); + + std::set<const Resource*> plugins = serialise_graph( + graph, + Sord::URI(_model->world(), main_file, _base_uri)); + + finish(); + write_manifest(path, graph); + write_plugins(path, plugins); + + _root_path = old_root_path; +} + +/** Begin a serialization to a file. + * + * This must be called before any serializing methods. + */ +void +Serialiser::Impl::start_to_file(const Raul::Path& root, + const FilePath& filename) +{ + _base_uri = URI(filename); + _basename = filename.stem(); + if (_basename == "main") { + _basename = filename.parent_path().stem(); + } + + _model = new Sord::Model(*_world.rdf_world(), _base_uri); + _mode = Mode::TO_FILE; + _root_path = root; +} + +void +Serialiser::start_to_string(const Raul::Path& root, const URI& base_uri) +{ + me->_root_path = root; + me->_base_uri = base_uri; + me->_model = new Sord::Model(*me->_world.rdf_world(), base_uri); + me->_mode = Impl::Mode::TO_STRING; +} + +void +Serialiser::start_to_file(const Raul::Path& root, const std::string& filename) +{ + me->start_to_file(root, filename); +} + +std::string +Serialiser::finish() +{ + return me->finish(); +} + +std::string +Serialiser::Impl::finish() +{ + std::string ret = ""; + if (_mode == Mode::TO_FILE) { + SerdStatus st = _model->write_to_file(_base_uri, SERD_TURTLE); + if (st) { + _world.log().error(fmt("Error writing file %1% (%2%)\n") + % _base_uri % serd_strerror(st)); + } + } else { + ret = _model->write_to_string(_base_uri, SERD_TURTLE); + } + + delete _model; + _model = nullptr; + _base_uri = URI(); + + return ret; +} + +Sord::Node +Serialiser::Impl::path_rdf_node(const Raul::Path& path) +{ + assert(_model); + assert(path == _root_path || path.is_child_of(_root_path)); + return Sord::URI(_model->world(), + path.substr(_root_path.base().length()), + _base_uri); +} + +void +Serialiser::serialise(SPtr<const Node> object) +{ + if (!me->_model) { + throw std::logic_error("serialise called without serialisation in progress"); + } + + if (object->graph_type() == Node::GraphType::GRAPH) { + me->serialise_graph(object, me->path_rdf_node(object->path())); + } else if (object->graph_type() == Node::GraphType::BLOCK) { + const Sord::URI plugin_id(me->_model->world(), object->plugin()->uri()); + me->serialise_block(object, plugin_id, me->path_rdf_node(object->path())); + } else if (object->graph_type() == Node::GraphType::PORT) { + me->serialise_port(object.get(), + Resource::Graph::DEFAULT, + me->path_rdf_node(object->path())); + } else { + me->serialise_properties(me->path_rdf_node(object->path()), + object->properties()); + } +} + +std::set<const Resource*> +Serialiser::Impl::serialise_graph(SPtr<const Node> graph, + const Sord::Node& graph_id) +{ + Sord::World& world = _model->world(); + const URIs& uris = _world.uris(); + + _model->add_statement(graph_id, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.ingen_Graph)); + + _model->add_statement(graph_id, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.lv2_Plugin)); + + _model->add_statement(graph_id, + Sord::URI(world, uris.lv2_extensionData), + Sord::URI(world, LV2_STATE__interface)); + + _model->add_statement(graph_id, + Sord::URI(world, LV2_UI__ui), + Sord::URI(world, "http://drobilla.net/ns/ingen#GraphUIGtk2")); + + // If the graph has no doap:name (required by LV2), use the basename + if (graph->properties().find(uris.doap_name) == graph->properties().end()) { + _model->add_statement(graph_id, + Sord::URI(world, uris.doap_name), + Sord::Literal(world, _basename)); + } + + const Properties props = graph->properties(Resource::Graph::INTERNAL); + serialise_properties(graph_id, props); + + std::set<const Resource*> plugins; + + const Store::const_range kids = _world.store()->children_range(graph); + for (Store::const_iterator n = kids.first; n != kids.second; ++n) { + if (n->first.parent() != graph->path()) { + continue; + } + + if (n->second->graph_type() == Node::GraphType::GRAPH) { + SPtr<Node> subgraph = n->second; + + SerdURI base_uri; + serd_uri_parse((const uint8_t*)_base_uri.c_str(), &base_uri); + + const std::string sub_bundle_path = subgraph->path().substr(1) + ".ingen"; + + SerdURI subgraph_uri; + SerdNode subgraph_node = serd_node_new_uri_from_string( + (const uint8_t*)sub_bundle_path.c_str(), + &base_uri, + &subgraph_uri); + + const Sord::URI subgraph_id(world, (const char*)subgraph_node.buf); + + // Save our state + URI my_base_uri = _base_uri; + Sord::Model* my_model = _model; + + // Write child bundle within this bundle + write_bundle(subgraph, subgraph_id); + + // Restore our state + _base_uri = my_base_uri; + _model = my_model; + + // Serialise reference to graph block + const Sord::Node block_id(path_rdf_node(subgraph->path())); + _model->add_statement(graph_id, + Sord::URI(world, uris.ingen_block), + block_id); + serialise_block(subgraph, subgraph_id, block_id); + } else if (n->second->graph_type() == Node::GraphType::BLOCK) { + SPtr<const Node> block = n->second; + + const Sord::URI class_id(world, block->plugin()->uri()); + const Sord::Node block_id(path_rdf_node(n->second->path())); + _model->add_statement(graph_id, + Sord::URI(world, uris.ingen_block), + block_id); + serialise_block(block, class_id, block_id); + + plugins.insert(block->plugin()); + } + } + + for (uint32_t i = 0; i < graph->num_ports(); ++i) { + Node* p = graph->port(i); + const Sord::Node port_id = path_rdf_node(p->path()); + + // Ensure lv2:name always exists so Graph is a valid LV2 plugin + if (p->properties().find(uris.lv2_name) == p->properties().end()) { + p->set_property(uris.lv2_name, + _world.forge().alloc(p->symbol().c_str())); + } + + _model->add_statement(graph_id, + Sord::URI(world, LV2_CORE__port), + port_id); + serialise_port(p, Resource::Graph::DEFAULT, port_id); + serialise_port(p, Resource::Graph::INTERNAL, port_id); + } + + for (const auto& a : graph->arcs()) { + serialise_arc(graph_id, a.second); + } + + return plugins; +} + +void +Serialiser::Impl::serialise_block(SPtr<const Node> block, + const Sord::Node& class_id, + const Sord::Node& block_id) +{ + const URIs& uris = _world.uris(); + + _model->add_statement(block_id, + Sord::URI(_model->world(), uris.rdf_type), + Sord::URI(_model->world(), uris.ingen_Block)); + _model->add_statement(block_id, + Sord::URI(_model->world(), uris.lv2_prototype), + class_id); + + // Serialise properties, but remove possibly stale state:state (set again below) + Properties props = block->properties(); + props.erase(uris.state_state); + serialise_properties(block_id, props); + + if (_base_uri.scheme() == "file") { + const FilePath base_path = _base_uri.file_path(); + const FilePath graph_dir = base_path.parent_path(); + const FilePath state_dir = graph_dir / block->symbol(); + const FilePath state_file = state_dir / "state.ttl"; + if (block->save_state(state_dir)) { + _model->add_statement(block_id, + Sord::URI(_model->world(), uris.state_state), + Sord::URI(_model->world(), URI(state_file))); + } + } + + for (uint32_t i = 0; i < block->num_ports(); ++i) { + Node* const p = block->port(i); + const Sord::Node port_id = path_rdf_node(p->path()); + serialise_port(p, Resource::Graph::DEFAULT, port_id); + _model->add_statement(block_id, + Sord::URI(_model->world(), uris.lv2_port), + port_id); + } +} + +void +Serialiser::Impl::serialise_port(const Node* port, + Resource::Graph context, + const Sord::Node& port_id) +{ + URIs& uris = _world.uris(); + Sord::World& world = _model->world(); + Properties props = port->properties(context); + + if (context == Resource::Graph::INTERNAL) { + // Always write lv2:symbol for Graph ports (required for lv2:Plugin) + _model->add_statement(port_id, + Sord::URI(world, uris.lv2_symbol), + Sord::Literal(world, port->path().symbol())); + } else if (context == Resource::Graph::EXTERNAL) { + // Never write lv2:index for plugin instances (not persistent/stable) + props.erase(uris.lv2_index); + } + + if (context == Resource::Graph::INTERNAL && + port->has_property(uris.rdf_type, uris.lv2_ControlPort) && + port->has_property(uris.rdf_type, uris.lv2_InputPort)) + { + const Atom& val = port->get_property(uris.ingen_value); + if (val.is_valid()) { + props.erase(uris.lv2_default); + props.emplace(uris.lv2_default, val); + } else { + _world.log().warn("Control input has no value, lv2:default omitted.\n"); + } + } else if (context != Resource::Graph::INTERNAL && + !port->has_property(uris.rdf_type, uris.lv2_InputPort)) { + props.erase(uris.ingen_value); + } + + serialise_properties(port_id, props); +} + +void +Serialiser::serialise_arc(const Sord::Node& parent, + SPtr<const Arc> arc) +{ + return me->serialise_arc(parent, arc); +} + +void +Serialiser::Impl::serialise_arc(const Sord::Node& parent, + SPtr<const Arc> arc) +{ + if (!_model) { + throw std::logic_error( + "serialise_arc called without serialisation in progress"); + } + + Sord::World& world = _model->world(); + const URIs& uris = _world.uris(); + + const Sord::Node src = path_rdf_node(arc->tail_path()); + const Sord::Node dst = path_rdf_node(arc->head_path()); + const Sord::Node arc_id = Sord::Node::blank_id(*_world.rdf_world()); + _model->add_statement(arc_id, + Sord::URI(world, uris.ingen_tail), + src); + _model->add_statement(arc_id, + Sord::URI(world, uris.ingen_head), + dst); + + if (parent.is_valid()) { + _model->add_statement(parent, + Sord::URI(world, uris.ingen_arc), + arc_id); + } else { + _model->add_statement(arc_id, + Sord::URI(world, uris.rdf_type), + Sord::URI(world, uris.ingen_Arc)); + } +} + +static bool +skip_property(Ingen::URIs& uris, const Sord::Node& predicate) +{ + return (predicate == INGEN__file || + predicate == uris.ingen_arc || + predicate == uris.ingen_block || + predicate == uris.lv2_port); +} + +void +Serialiser::Impl::serialise_properties(Sord::Node id, + const Properties& props) +{ + LV2_URID_Unmap* unmap = &_world.uri_map().urid_unmap_feature()->urid_unmap; + SerdNode base = serd_node_from_string(SERD_URI, + (const uint8_t*)_base_uri.c_str()); + SerdEnv* env = serd_env_new(&base); + SordInserter* inserter = sord_inserter_new(_model->c_obj(), env); + + sratom_set_sink(_sratom, _base_uri.c_str(), + (SerdStatementSink)sord_inserter_write_statement, nullptr, + inserter); + + sratom_set_pretty_numbers(_sratom, true); + + for (const auto& p : props) { + const Sord::URI key(_model->world(), p.first); + if (!skip_property(_world.uris(), key)) { + if (p.second.type() == _world.uris().atom_URI && + !strncmp((const char*)p.second.get_body(), "ingen:/main/", 13)) { + /* Value is a graph URI relative to the running engine. + Chop the prefix and save the path relative to the graph file. + This allows saving references to bundle resources. */ + sratom_write(_sratom, unmap, 0, + sord_node_to_serd_node(id.c_obj()), + sord_node_to_serd_node(key.c_obj()), + p.second.type(), p.second.size(), + (const char*)p.second.get_body() + 13); + } else { + sratom_write(_sratom, unmap, 0, + sord_node_to_serd_node(id.c_obj()), + sord_node_to_serd_node(key.c_obj()), + p.second.type(), p.second.size(), p.second.get_body()); + } + } + } + + sord_inserter_free(inserter); + serd_env_free(env); +} + +} // namespace Ingen diff --git a/src/SocketReader.cpp b/src/SocketReader.cpp new file mode 100644 index 00000000..13e95430 --- /dev/null +++ b/src/SocketReader.cpp @@ -0,0 +1,199 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cerrno> + +#include <poll.h> + +#include "ingen/AtomForgeSink.hpp" +#include "ingen/AtomReader.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/SocketReader.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "raul/Socket.hpp" +#include "sord/sordmm.hpp" +#include "sratom/sratom.h" + +namespace Ingen { + +SocketReader::SocketReader(Ingen::World& world, + Interface& iface, + SPtr<Raul::Socket> sock) + : _world(world) + , _iface(iface) + , _inserter(nullptr) + , _msg_node(nullptr) + , _socket(std::move(sock)) + , _exit_flag(false) + , _thread(&SocketReader::run, this) +{} + +SocketReader::~SocketReader() +{ + _exit_flag = true; + _socket->shutdown(); + _thread.join(); +} + +SerdStatus +SocketReader::set_base_uri(SocketReader* iface, + const SerdNode* uri_node) +{ + return sord_inserter_set_base_uri(iface->_inserter, uri_node); +} + +SerdStatus +SocketReader::set_prefix(SocketReader* iface, + const SerdNode* name, + const SerdNode* uri_node) +{ + return sord_inserter_set_prefix(iface->_inserter, name, uri_node); +} + +SerdStatus +SocketReader::write_statement(SocketReader* iface, + SerdStatementFlags flags, + const SerdNode* graph, + const SerdNode* subject, + const SerdNode* predicate, + const SerdNode* object, + const SerdNode* object_datatype, + const SerdNode* object_lang) +{ + if (!iface->_msg_node) { + iface->_msg_node = sord_node_from_serd_node( + iface->_world.rdf_world()->c_obj(), iface->_env, subject, nullptr, nullptr); + } + + return sord_inserter_write_statement( + iface->_inserter, flags, graph, + subject, predicate, object, + object_datatype, object_lang); +} + +void +SocketReader::run() +{ + Sord::World* world = _world.rdf_world(); + LV2_URID_Map* map = &_world.uri_map().urid_map_feature()->urid_map; + + // Open socket as a FILE for reading directly with serd + FILE* f = fdopen(_socket->fd(), "r"); + if (!f) { + _world.log().error(fmt("Failed to open connection (%1%)\n") + % strerror(errno)); + // Connection gone, exit + _socket.reset(); + return; + } + + // Set up sratom and a forge to build LV2 atoms from model + Sratom* sratom = sratom_new(map); + LV2_Atom_Forge forge; + lv2_atom_forge_init(&forge, map); + + AtomForgeSink buffer(&forge); + + SordNode* base_uri = nullptr; + SordModel* model = nullptr; + { + // Lock RDF world + std::lock_guard<std::mutex> lock(_world.rdf_mutex()); + + // Use <ingen:/> as base URI, so relative URIs are like bundle paths + base_uri = sord_new_uri(world->c_obj(), (const uint8_t*)"ingen:/"); + + // Make a model and reader to parse the next Turtle message + _env = world->prefixes().c_obj(); + model = sord_new(world->c_obj(), SORD_SPO, false); + + // Create an inserter for writing incoming triples to model + _inserter = sord_inserter_new(model, _env); + } + + SerdReader* reader = serd_reader_new( + SERD_TURTLE, this, nullptr, + (SerdBaseSink)set_base_uri, + (SerdPrefixSink)set_prefix, + (SerdStatementSink)write_statement, + nullptr); + + serd_env_set_base_uri(_env, sord_node_to_serd_node(base_uri)); + serd_reader_start_stream(reader, f, (const uint8_t*)"(socket)", false); + + // Make an AtomReader to call Ingen Interface methods based on Atom + AtomReader ar(_world.uri_map(), _world.uris(), _world.log(), _iface); + + struct pollfd pfd; + pfd.fd = _socket->fd(); + pfd.events = POLLIN; + pfd.revents = 0; + + while (!_exit_flag) { + if (feof(f)) { + break; // Lost connection + } + + // Wait for input to arrive at socket + int ret = poll(&pfd, 1, -1); + if (ret == -1 || (pfd.revents & (POLLERR|POLLHUP|POLLNVAL))) { + on_hangup(); + break; // Hangup + } else if (!ret) { + continue; // No data, shouldn't happen + } + + // Lock RDF world + std::lock_guard<std::mutex> lock(_world.rdf_mutex()); + + // Read until the next '.' + SerdStatus st = serd_reader_read_chunk(reader); + if (st == SERD_FAILURE || !_msg_node) { + continue; // Read nothing, e.g. just whitespace + } else if (st) { + _world.log().error(fmt("Read error: %1%\n") + % serd_strerror(st)); + continue; + } + + // Build an LV2_Atom at chunk.buf from the message + sratom_read(sratom, &forge, world->c_obj(), model, _msg_node); + + // Call _iface methods based on atom content + ar.write(buffer.atom()); + + // Reset everything for the next iteration + buffer.clear(); + sord_node_free(world->c_obj(), _msg_node); + _msg_node = nullptr; + } + + // Lock RDF world + std::lock_guard<std::mutex> lock(_world.rdf_mutex()); + + // Destroy everything + fclose(f); + sord_inserter_free(_inserter); + serd_reader_end_stream(reader); + sratom_free(sratom); + serd_reader_free(reader); + sord_free(model); + _socket.reset(); +} + +} // namespace Ingen diff --git a/src/SocketWriter.cpp b/src/SocketWriter.cpp new file mode 100644 index 00000000..68091bcc --- /dev/null +++ b/src/SocketWriter.cpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2012-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> + +#include "ingen/SocketWriter.hpp" +#include "raul/Socket.hpp" + +#ifndef MSG_NOSIGNAL +# define MSG_NOSIGNAL 0 +#endif + +namespace Ingen { + +SocketWriter::SocketWriter(URIMap& map, + URIs& uris, + const URI& uri, + SPtr<Raul::Socket> sock) + : TurtleWriter(map, uris, uri) + , _socket(std::move(sock)) +{} + +size_t +SocketWriter::text_sink(const void* buf, size_t len) +{ + ssize_t ret = send(_socket->fd(), buf, len, MSG_NOSIGNAL); + if (ret < 0) { + return 0; + } + return ret; +} + +void +SocketWriter::bundle_end() +{ + TurtleWriter::bundle_end(); + + // Send a NULL byte to indicate end of bundle + const char end[] = { 0 }; + send(_socket->fd(), end, 1, MSG_NOSIGNAL); +} + +} // namespace Ingen diff --git a/src/Store.cpp b/src/Store.cpp new file mode 100644 index 00000000..327ce416 --- /dev/null +++ b/src/Store.cpp @@ -0,0 +1,143 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <sstream> + +#include "ingen/Node.hpp" +#include "ingen/Store.hpp" + +namespace Ingen { + +void +Store::add(Node* o) +{ + if (find(o->path()) != end()) { + return; + } + + emplace(o->path(), SPtr<Node>(o)); + + for (uint32_t i = 0; i < o->num_ports(); ++i) { + add(o->port(i)); + } +} + +/* + TODO: These methods are currently O(n_children) but should logarithmic. The + std::map methods do not allow passing a comparator, but std::upper_bound + does. This should be achievable by making a rooted comparator that is a + normal ordering except compares a special sentinel value as the greatest + element that is a child of the parent. Searching for this sentinel should + then find the end of the descendants in logarithmic time. +*/ + +Store::iterator +Store::find_descendants_end(const iterator parent) +{ + iterator descendants_end = parent; + ++descendants_end; + while (descendants_end != end() && + descendants_end->first.is_child_of(parent->first)) { + ++descendants_end; + } + + return descendants_end; +} + +Store::const_iterator +Store::find_descendants_end(const const_iterator parent) const +{ + const_iterator descendants_end = parent; + ++descendants_end; + while (descendants_end != end() && + descendants_end->first.is_child_of(parent->first)) { + ++descendants_end; + } + + return descendants_end; +} + +Store::const_range +Store::children_range(SPtr<const Node> o) const +{ + const const_iterator parent = find(o->path()); + if (parent != end()) { + const_iterator first_child = parent; + ++first_child; + return std::make_pair(first_child, find_descendants_end(parent)); + } + return make_pair(end(), end()); +} + +void +Store::remove(const iterator top, Objects& removed) +{ + if (top != end()) { + const iterator descendants_end = find_descendants_end(top); + removed.insert(top, descendants_end); + erase(top, descendants_end); + } +} + +void +Store::rename(const iterator top, const Raul::Path& new_path) +{ + const Raul::Path old_path = top->first; + + // Remove the object and all its descendants + Objects removed; + remove(top, removed); + + // Rename all the removed objects + for (Objects::const_iterator i = removed.begin(); i != removed.end(); ++i) { + const Raul::Path path = (i->first == old_path) + ? new_path + : new_path.child( + Raul::Path(i->first.substr(old_path.base().length() - 1))); + + i->second->set_path(path); + assert(find(path) == end()); // Shouldn't be dropping objects! + emplace(path, i->second); + } +} + +unsigned +Store::child_name_offset(const Raul::Path& parent, + const Raul::Symbol& symbol, + bool allow_zero) const +{ + unsigned offset = 0; + + while (true) { + std::stringstream ss; + ss << symbol; + if (offset > 0) { + ss << "_" << offset; + } + if (find(parent.child(Raul::Symbol(ss.str()))) == end() && + (allow_zero || offset > 0)) { + break; + } else if (offset == 0) { + offset = 2; + } else { + ++offset; + } + } + + return offset; +} + +} // namespace Ingen diff --git a/src/StreamWriter.cpp b/src/StreamWriter.cpp new file mode 100644 index 00000000..45853055 --- /dev/null +++ b/src/StreamWriter.cpp @@ -0,0 +1,39 @@ +/* + This file is part of Ingen. + Copyright 2012-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/ColorContext.hpp" +#include "ingen/StreamWriter.hpp" + +namespace Ingen { + +StreamWriter::StreamWriter(URIMap& map, + URIs& uris, + const URI& uri, + FILE* stream, + ColorContext::Color color) + : TurtleWriter(map, uris, uri) + , _stream(stream) + , _color(color) +{} + +size_t +StreamWriter::text_sink(const void* buf, size_t len) +{ + ColorContext ctx(_stream, _color); + return fwrite(buf, 1, len, _stream); +} + +} // namespace Ingen diff --git a/src/TurtleWriter.cpp b/src/TurtleWriter.cpp new file mode 100644 index 00000000..368184d4 --- /dev/null +++ b/src/TurtleWriter.cpp @@ -0,0 +1,101 @@ +/* + This file is part of Ingen. + Copyright 2012-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/TurtleWriter.hpp" +#include "ingen/URIMap.hpp" + +#define USTR(s) ((const uint8_t*)(s)) + +namespace Ingen { + +static size_t +c_text_sink(const void* buf, size_t len, void* stream) +{ + TurtleWriter* writer = (TurtleWriter*)stream; + return writer->text_sink(buf, len); +} + +static SerdStatus +write_prefix(void* handle, const SerdNode* name, const SerdNode* uri) +{ + serd_writer_set_prefix((SerdWriter*)handle, name, uri); + return SERD_SUCCESS; +} + +TurtleWriter::TurtleWriter(URIMap& map, URIs& uris, const URI& uri) + : AtomWriter(map, uris, *this) + , _map(map) + , _sratom(sratom_new(&map.urid_map_feature()->urid_map)) + , _uri(uri) + , _wrote_prefixes(false) +{ + // Use <ingen:/> as base URI, so relative URIs are like bundle paths + _base = serd_node_from_string(SERD_URI, (const uint8_t*)"ingen:/"); + serd_uri_parse(_base.buf, &_base_uri); + + // Set up serialisation environment + _env = serd_env_new(&_base); + serd_env_set_prefix_from_strings(_env, USTR("atom"), USTR("http://lv2plug.in/ns/ext/atom#")); + serd_env_set_prefix_from_strings(_env, USTR("doap"), USTR("http://usefulinc.com/ns/doap#")); + serd_env_set_prefix_from_strings(_env, USTR("ingen"), USTR(INGEN_NS)); + serd_env_set_prefix_from_strings(_env, USTR("lv2"), USTR("http://lv2plug.in/ns/lv2core#")); + serd_env_set_prefix_from_strings(_env, USTR("midi"), USTR("http://lv2plug.in/ns/ext/midi#")); + serd_env_set_prefix_from_strings(_env, USTR("owl"), USTR("http://www.w3.org/2002/07/owl#")); + serd_env_set_prefix_from_strings(_env, USTR("patch"), USTR("http://lv2plug.in/ns/ext/patch#")); + serd_env_set_prefix_from_strings(_env, USTR("rdf"), USTR("http://www.w3.org/1999/02/22-rdf-syntax-ns#")); + serd_env_set_prefix_from_strings(_env, USTR("rdfs"), USTR("http://www.w3.org/2000/01/rdf-schema#")); + serd_env_set_prefix_from_strings(_env, USTR("xsd"), USTR("http://www.w3.org/2001/XMLSchema#")); + + // Make a Turtle writer that writes to text_sink + _writer = serd_writer_new( + SERD_TURTLE, + (SerdStyle)(SERD_STYLE_RESOLVED|SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED), + _env, + &_base_uri, + c_text_sink, + this); + + // Configure sratom to write directly to the writer (and thus text_sink) + sratom_set_sink(_sratom, + (const char*)_base.buf, + (SerdStatementSink)serd_writer_write_statement, + (SerdEndSink)serd_writer_end_anon, + _writer); +} + +TurtleWriter::~TurtleWriter() +{ + sratom_free(_sratom); + serd_writer_free(_writer); + serd_env_free(_env); +} + +bool +TurtleWriter::write(const LV2_Atom* msg, int32_t default_id) +{ + if (!_wrote_prefixes) { + // Write namespace prefixes once to reduce traffic + serd_env_foreach(_env, write_prefix, _writer); + _wrote_prefixes = true; + } + + sratom_write(_sratom, &_map.urid_unmap_feature()->urid_unmap, 0, + nullptr, nullptr, msg->type, msg->size, LV2_ATOM_BODY_CONST(msg)); + serd_writer_finish(_writer); + return true; +} + +} // namespace Ingen diff --git a/src/URI.cpp b/src/URI.cpp new file mode 100644 index 00000000..3e2d2a29 --- /dev/null +++ b/src/URI.cpp @@ -0,0 +1,113 @@ +/* + This file is part of Ingen. + Copyright 2018 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> + +#include "ingen/FilePath.hpp" +#include "ingen/URI.hpp" + +namespace Ingen { + +URI::URI() + : _node(SERD_NODE_NULL) + , _uri(SERD_URI_NULL) +{} + +URI::URI(const std::string& str) + : _node(serd_node_new_uri_from_string((const uint8_t*)str.c_str(), + NULL, + &_uri)) +{} + +URI::URI(const char* str) + : _node(serd_node_new_uri_from_string((const uint8_t*)str, NULL, &_uri)) +{} + +URI::URI(const std::string& str, const URI& base) + : _node(serd_node_new_uri_from_string((const uint8_t*)str.c_str(), + &base._uri, + &_uri)) +{} + +URI::URI(SerdNode node) + : _node(serd_node_new_uri_from_node(&node, NULL, &_uri)) +{ + assert(node.type == SERD_URI); +} + +URI::URI(SerdNode node, SerdURI uri) + : _node(node) + , _uri(uri) +{ + assert(node.type == SERD_URI); +} + +URI::URI(const Sord::Node& node) + : URI(*node.to_serd_node()) +{ +} + +URI::URI(const FilePath& path) + : _node(serd_node_new_file_uri((const uint8_t*)path.c_str(), + NULL, + &_uri, + true)) +{} + +URI::URI(const URI& uri) + : _node(serd_node_new_uri(&uri._uri, NULL, &_uri)) +{} + +URI& +URI::operator=(const URI& uri) +{ + serd_node_free(&_node); + _node = serd_node_new_uri(&uri._uri, NULL, &_uri); + return *this; +} + +URI::URI(URI&& uri) + : _node(uri._node) + , _uri(uri._uri) +{ + uri._node = SERD_NODE_NULL; + uri._uri = SERD_URI_NULL; +} + +URI& +URI::operator=(URI&& uri) +{ + _node = uri._node; + _uri = uri._uri; + uri._node = SERD_NODE_NULL; + uri._uri = SERD_URI_NULL; + return *this; +} + +URI::~URI() +{ + serd_node_free(&_node); +} + +URI +URI::make_relative(const URI& base) const +{ + SerdURI uri; + SerdNode node = serd_node_new_relative_uri(&_uri, &base._uri, NULL, &uri); + return URI(node, uri); +} + +} // namespace Ingen diff --git a/src/URIMap.cpp b/src/URIMap.cpp new file mode 100644 index 00000000..9ce1f178 --- /dev/null +++ b/src/URIMap.cpp @@ -0,0 +1,123 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdint> + +#include "ingen/Log.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" + +namespace Ingen { + +URIMap::URIMap(Log& log, LV2_URID_Map* map, LV2_URID_Unmap* unmap) + : _urid_map_feature(new URIDMapFeature(this, map, log)) + , _urid_unmap_feature(new URIDUnmapFeature(this, unmap)) +{ +} + +URIMap::URIDMapFeature::URIDMapFeature(URIMap* map, + LV2_URID_Map* impl, + Log& log) + : Feature(LV2_URID__map, &urid_map) + , log(log) +{ + if (impl) { + urid_map = *impl; + } else { + urid_map.map = default_map; + urid_map.handle = map; + } +} + +LV2_URID +URIMap::URIDMapFeature::default_map(LV2_URID_Map_Handle h, + const char* c_uri) +{ + URIMap* const map((URIMap*)h); + std::string uri(c_uri); + std::lock_guard<std::mutex> lock(map->_mutex); + + auto record = map->_map.emplace(uri, map->_map.size() + 1); + const auto id = record.first->second; + if (record.second) { + assert(id == map->_map.size()); + assert(id == map->_unmap.size() + 1); + map->_unmap.emplace_back(std::move(uri)); + } + return id; +} + +LV2_URID +URIMap::URIDMapFeature::map(const char* uri) +{ + if (!URI::is_valid(uri)) { + log.error(fmt("Attempt to map invalid URI <%1%>\n") % uri); + return 0; + } + return urid_map.map(urid_map.handle, uri); +} + +URIMap::URIDUnmapFeature::URIDUnmapFeature(URIMap* map, + LV2_URID_Unmap* impl) + : Feature(LV2_URID__unmap, &urid_unmap) +{ + if (impl) { + urid_unmap = *impl; + } else { + urid_unmap.unmap = default_unmap; + urid_unmap.handle = map; + } +} + +const char* +URIMap::URIDUnmapFeature::default_unmap(LV2_URID_Unmap_Handle h, + LV2_URID urid) +{ + URIMap* const map((URIMap*)h); + std::lock_guard<std::mutex> lock(map->_mutex); + + return (urid > 0 && urid <= map->_unmap.size() + ? map->_unmap[urid - 1].c_str() + : NULL); +} + +const char* +URIMap::URIDUnmapFeature::unmap(LV2_URID urid) +{ + return urid_unmap.unmap(urid_unmap.handle, urid); +} + +uint32_t +URIMap::map_uri(const char* uri) +{ + const uint32_t urid = _urid_map_feature->map(uri); +#ifdef INGEN_DEBUG_URIDS + fprintf(stderr, "Map URI %3u <= %s\n", urid, uri); +#endif + return urid; +} + +const char* +URIMap::unmap_uri(uint32_t urid) const +{ + const char* uri = _urid_unmap_feature->unmap(urid); +#ifdef INGEN_DEBUG_URIDS + fprintf(stderr, "Unmap URI %3u => %s\n", urid, uri); +#endif + return uri; +} + +} // namespace Ingen diff --git a/src/URIs.cpp b/src/URIs.cpp new file mode 100644 index 00000000..af03b7b5 --- /dev/null +++ b/src/URIs.cpp @@ -0,0 +1,204 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "lv2/lv2plug.in/ns/ext/morph/morph.h" +#include "lv2/lv2plug.in/ns/ext/options/options.h" +#include "lv2/lv2plug.in/ns/ext/parameters/parameters.h" +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "lv2/lv2plug.in/ns/ext/port-props/port-props.h" +#include "lv2/lv2plug.in/ns/ext/presets/presets.h" +#include "lv2/lv2plug.in/ns/ext/resize-port/resize-port.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/ext/time/time.h" +#include "lv2/lv2plug.in/ns/ext/worker/worker.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +namespace Ingen { + +URIs::Quark::Quark(Forge& forge, + URIMap* map, + LilvWorld* lworld, + const char* str) + : URI(str) + , urid(forge.make_urid(URI(str))) + , uri(forge.alloc_uri(str)) + , lnode(lilv_new_uri(lworld, str)) +{} + +URIs::Quark::Quark(const Quark& copy) + : URI(copy) + , urid(copy.urid) + , uri(copy.uri) + , lnode(lilv_node_duplicate(copy.lnode)) +{} + +URIs::Quark::~Quark() +{ + lilv_node_free(lnode); +} + +#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define NS_RDFS "http://www.w3.org/2000/01/rdf-schema#" + +URIs::URIs(Forge& forge, URIMap* map, LilvWorld* lworld) + : forge(forge) + , atom_AtomPort (forge, map, lworld, LV2_ATOM__AtomPort) + , atom_Bool (forge, map, lworld, LV2_ATOM__Bool) + , atom_Chunk (forge, map, lworld, LV2_ATOM__Chunk) + , atom_Float (forge, map, lworld, LV2_ATOM__Float) + , atom_Int (forge, map, lworld, LV2_ATOM__Int) + , atom_Object (forge, map, lworld, LV2_ATOM__Object) + , atom_Path (forge, map, lworld, LV2_ATOM__Path) + , atom_Sequence (forge, map, lworld, LV2_ATOM__Sequence) + , atom_Sound (forge, map, lworld, LV2_ATOM__Sound) + , atom_String (forge, map, lworld, LV2_ATOM__String) + , atom_URI (forge, map, lworld, LV2_ATOM__URI) + , atom_URID (forge, map, lworld, LV2_ATOM__URID) + , atom_bufferType (forge, map, lworld, LV2_ATOM__bufferType) + , atom_eventTransfer (forge, map, lworld, LV2_ATOM__eventTransfer) + , atom_supports (forge, map, lworld, LV2_ATOM__supports) + , bufsz_maxBlockLength (forge, map, lworld, LV2_BUF_SIZE__maxBlockLength) + , bufsz_minBlockLength (forge, map, lworld, LV2_BUF_SIZE__minBlockLength) + , bufsz_sequenceSize (forge, map, lworld, LV2_BUF_SIZE__sequenceSize) + , doap_name (forge, map, lworld, "http://usefulinc.com/ns/doap#name") + , ingen_Arc (forge, map, lworld, INGEN__Arc) + , ingen_Block (forge, map, lworld, INGEN__Block) + , ingen_BundleEnd (forge, map, lworld, INGEN__BundleEnd) + , ingen_BundleStart (forge, map, lworld, INGEN__BundleStart) + , ingen_Graph (forge, map, lworld, INGEN__Graph) + , ingen_GraphPrototype (forge, map, lworld, INGEN__GraphPrototype) + , ingen_Internal (forge, map, lworld, INGEN__Internal) + , ingen_Redo (forge, map, lworld, INGEN__Redo) + , ingen_Undo (forge, map, lworld, INGEN__Undo) + , ingen_activity (forge, map, lworld, INGEN__activity) + , ingen_arc (forge, map, lworld, INGEN__arc) + , ingen_block (forge, map, lworld, INGEN__block) + , ingen_broadcast (forge, map, lworld, INGEN__broadcast) + , ingen_canvasX (forge, map, lworld, INGEN__canvasX) + , ingen_canvasY (forge, map, lworld, INGEN__canvasY) + , ingen_enabled (forge, map, lworld, INGEN__enabled) + , ingen_externalContext (forge, map, lworld, INGEN__externalContext) + , ingen_file (forge, map, lworld, INGEN__file) + , ingen_head (forge, map, lworld, INGEN__head) + , ingen_incidentTo (forge, map, lworld, INGEN__incidentTo) + , ingen_internalContext (forge, map, lworld, INGEN__internalContext) + , ingen_loadedBundle (forge, map, lworld, INGEN__loadedBundle) + , ingen_maxRunLoad (forge, map, lworld, INGEN__maxRunLoad) + , ingen_meanRunLoad (forge, map, lworld, INGEN__meanRunLoad) + , ingen_minRunLoad (forge, map, lworld, INGEN__minRunLoad) + , ingen_numThreads (forge, map, lworld, INGEN__numThreads) + , ingen_polyphonic (forge, map, lworld, INGEN__polyphonic) + , ingen_polyphony (forge, map, lworld, INGEN__polyphony) + , ingen_prototype (forge, map, lworld, INGEN__prototype) + , ingen_sprungLayout (forge, map, lworld, INGEN__sprungLayout) + , ingen_tail (forge, map, lworld, INGEN__tail) + , ingen_uiEmbedded (forge, map, lworld, INGEN__uiEmbedded) + , ingen_value (forge, map, lworld, INGEN__value) + , log_Error (forge, map, lworld, LV2_LOG__Error) + , log_Note (forge, map, lworld, LV2_LOG__Note) + , log_Trace (forge, map, lworld, LV2_LOG__Trace) + , log_Warning (forge, map, lworld, LV2_LOG__Warning) + , lv2_AudioPort (forge, map, lworld, LV2_CORE__AudioPort) + , lv2_CVPort (forge, map, lworld, LV2_CORE__CVPort) + , lv2_ControlPort (forge, map, lworld, LV2_CORE__ControlPort) + , lv2_InputPort (forge, map, lworld, LV2_CORE__InputPort) + , lv2_OutputPort (forge, map, lworld, LV2_CORE__OutputPort) + , lv2_Plugin (forge, map, lworld, LV2_CORE__Plugin) + , lv2_appliesTo (forge, map, lworld, LV2_CORE__appliesTo) + , lv2_binary (forge, map, lworld, LV2_CORE__binary) + , lv2_connectionOptional(forge, map, lworld, LV2_CORE__connectionOptional) + , lv2_control (forge, map, lworld, LV2_CORE__control) + , lv2_default (forge, map, lworld, LV2_CORE__default) + , lv2_designation (forge, map, lworld, LV2_CORE__designation) + , lv2_enumeration (forge, map, lworld, LV2_CORE__enumeration) + , lv2_extensionData (forge, map, lworld, LV2_CORE__extensionData) + , lv2_index (forge, map, lworld, LV2_CORE__index) + , lv2_integer (forge, map, lworld, LV2_CORE__integer) + , lv2_maximum (forge, map, lworld, LV2_CORE__maximum) + , lv2_microVersion (forge, map, lworld, LV2_CORE__microVersion) + , lv2_minimum (forge, map, lworld, LV2_CORE__minimum) + , lv2_minorVersion (forge, map, lworld, LV2_CORE__minorVersion) + , lv2_name (forge, map, lworld, LV2_CORE__name) + , lv2_port (forge, map, lworld, LV2_CORE__port) + , lv2_portProperty (forge, map, lworld, LV2_CORE__portProperty) + , lv2_prototype (forge, map, lworld, LV2_CORE__prototype) + , lv2_sampleRate (forge, map, lworld, LV2_CORE__sampleRate) + , lv2_scalePoint (forge, map, lworld, LV2_CORE__scalePoint) + , lv2_symbol (forge, map, lworld, LV2_CORE__symbol) + , lv2_toggled (forge, map, lworld, LV2_CORE__toggled) + , midi_Bender (forge, map, lworld, LV2_MIDI__Bender) + , midi_ChannelPressure (forge, map, lworld, LV2_MIDI__ChannelPressure) + , midi_Controller (forge, map, lworld, LV2_MIDI__Controller) + , midi_MidiEvent (forge, map, lworld, LV2_MIDI__MidiEvent) + , midi_NoteOn (forge, map, lworld, LV2_MIDI__NoteOn) + , midi_binding (forge, map, lworld, LV2_MIDI__binding) + , midi_controllerNumber (forge, map, lworld, LV2_MIDI__controllerNumber) + , midi_noteNumber (forge, map, lworld, LV2_MIDI__noteNumber) + , morph_AutoMorphPort (forge, map, lworld, LV2_MORPH__AutoMorphPort) + , morph_MorphPort (forge, map, lworld, LV2_MORPH__MorphPort) + , morph_currentType (forge, map, lworld, LV2_MORPH__currentType) + , morph_supportsType (forge, map, lworld, LV2_MORPH__supportsType) + , opt_interface (forge, map, lworld, LV2_OPTIONS__interface) + , param_sampleRate (forge, map, lworld, LV2_PARAMETERS__sampleRate) + , patch_Copy (forge, map, lworld, LV2_PATCH__Copy) + , patch_Delete (forge, map, lworld, LV2_PATCH__Delete) + , patch_Get (forge, map, lworld, LV2_PATCH__Get) + , patch_Message (forge, map, lworld, LV2_PATCH__Message) + , patch_Move (forge, map, lworld, LV2_PATCH__Move) + , patch_Patch (forge, map, lworld, LV2_PATCH__Patch) + , patch_Put (forge, map, lworld, LV2_PATCH__Put) + , patch_Response (forge, map, lworld, LV2_PATCH__Response) + , patch_Set (forge, map, lworld, LV2_PATCH__Set) + , patch_add (forge, map, lworld, LV2_PATCH__add) + , patch_body (forge, map, lworld, LV2_PATCH__body) + , patch_context (forge, map, lworld, LV2_PATCH__context) + , patch_destination (forge, map, lworld, LV2_PATCH__destination) + , patch_property (forge, map, lworld, LV2_PATCH__property) + , patch_remove (forge, map, lworld, LV2_PATCH__remove) + , patch_sequenceNumber (forge, map, lworld, LV2_PATCH__sequenceNumber) + , patch_subject (forge, map, lworld, LV2_PATCH__subject) + , patch_value (forge, map, lworld, LV2_PATCH__value) + , patch_wildcard (forge, map, lworld, LV2_PATCH__wildcard) + , pprops_logarithmic (forge, map, lworld, LV2_PORT_PROPS__logarithmic) + , pset_Preset (forge, map, lworld, LV2_PRESETS__Preset) + , pset_preset (forge, map, lworld, LV2_PRESETS__preset) + , rdf_type (forge, map, lworld, NS_RDF "type") + , rdfs_Class (forge, map, lworld, NS_RDFS "Class") + , rdfs_label (forge, map, lworld, NS_RDFS "label") + , rdfs_seeAlso (forge, map, lworld, NS_RDFS "seeAlso") + , rsz_minimumSize (forge, map, lworld, LV2_RESIZE_PORT__minimumSize) + , state_loadDefaultState(forge, map, lworld, LV2_STATE__loadDefaultState) + , state_state (forge, map, lworld, LV2_STATE__state) + , time_Position (forge, map, lworld, LV2_TIME__Position) + , time_bar (forge, map, lworld, LV2_TIME__bar) + , time_barBeat (forge, map, lworld, LV2_TIME__barBeat) + , time_beatUnit (forge, map, lworld, LV2_TIME__beatUnit) + , time_beatsPerBar (forge, map, lworld, LV2_TIME__beatsPerBar) + , time_beatsPerMinute (forge, map, lworld, LV2_TIME__beatsPerMinute) + , time_frame (forge, map, lworld, LV2_TIME__frame) + , time_speed (forge, map, lworld, LV2_TIME__speed) + , work_schedule (forge, map, lworld, LV2_WORKER__schedule) +{} + +} // namespace Ingen diff --git a/src/World.cpp b/src/World.cpp new file mode 100644 index 00000000..568ab405 --- /dev/null +++ b/src/World.cpp @@ -0,0 +1,355 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <map> +#include <memory> +#include <string> +#include <utility> + +#include "ingen/Configuration.hpp" +#include "ingen/DataAccess.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Forge.hpp" +#include "ingen/InstanceAccess.hpp" +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/Parser.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/filesystem.hpp" +#include "ingen/ingen.h" +#include "ingen/runtime_paths.hpp" +#include "lilv/lilv.h" +#include "sord/sordmm.hpp" + +using std::string; + +namespace Ingen { + +class EngineBase; +class Interface; +class Parser; +class Serialiser; +class Store; + +/** Load a dynamic module from the default path. + * + * This will check in the directories specified in the environment variable + * INGEN_MODULE_PATH (typical colon delimited format), then the default module + * installation directory (ie /usr/local/lib/ingen), in that order. + * + * \param name The base name of the module, e.g. "ingen_jack" + */ +static std::unique_ptr<Library> +ingen_load_library(Log& log, const string& name) +{ + std::unique_ptr<Library> library; + + // Search INGEN_MODULE_PATH first + const char* const module_path = getenv("INGEN_MODULE_PATH"); + if (module_path) { + string dir; + std::istringstream iss(module_path); + while (getline(iss, dir, search_path_separator)) { + FilePath filename = Ingen::ingen_module_path(name, FilePath(dir)); + if (filesystem::exists(filename)) { + library = std::unique_ptr<Library>(new Library(filename)); + if (*library) { + return library; + } else { + log.error(Library::get_last_error()); + } + } + } + } + + // Try default directory if not found + library = std::unique_ptr<Library>(new Library(Ingen::ingen_module_path(name))); + + if (*library) { + return library; + } else if (!module_path) { + log.error(fmt("Unable to find %1% (%2%)\n") + % name % Library::get_last_error()); + return nullptr; + } else { + log.error(fmt("Unable to load %1% from %2% (%3%)\n") + % name % module_path % Library::get_last_error()); + return nullptr; + } +} + +class World::Impl { +public: + Impl(LV2_URID_Map* map, + LV2_URID_Unmap* unmap, + LV2_Log_Log* lv2_log) + : argc(nullptr) + , argv(nullptr) + , lv2_features(nullptr) + , rdf_world(new Sord::World()) + , lilv_world(lilv_world_new()) + , uri_map(new URIMap(log, map, unmap)) + , forge(new Forge(*uri_map)) + , uris(new URIs(*forge, uri_map, lilv_world)) + , conf(*forge) + , log(lv2_log, *uris) + { + lv2_features = new LV2Features(); + lv2_features->add_feature(uri_map->urid_map_feature()); + lv2_features->add_feature(uri_map->urid_unmap_feature()); + lv2_features->add_feature(SPtr<InstanceAccess>(new InstanceAccess())); + lv2_features->add_feature(SPtr<DataAccess>(new DataAccess())); + lv2_features->add_feature(SPtr<Log::Feature>(new Log::Feature())); + lilv_world_load_all(lilv_world); + + // Set up RDF namespaces + rdf_world->add_prefix("atom", "http://lv2plug.in/ns/ext/atom#"); + rdf_world->add_prefix("doap", "http://usefulinc.com/ns/doap#"); + rdf_world->add_prefix("ingen", INGEN_NS); + rdf_world->add_prefix("lv2", "http://lv2plug.in/ns/lv2core#"); + rdf_world->add_prefix("midi", "http://lv2plug.in/ns/ext/midi#"); + rdf_world->add_prefix("owl", "http://www.w3.org/2002/07/owl#"); + rdf_world->add_prefix("patch", "http://lv2plug.in/ns/ext/patch#"); + rdf_world->add_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); + rdf_world->add_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#"); + rdf_world->add_prefix("xsd", "http://www.w3.org/2001/XMLSchema#"); + + // Load internal 'plugin' information into lilv world + LilvNode* rdf_type = lilv_new_uri( + lilv_world, "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); + LilvNode* ingen_Plugin = lilv_new_uri( + lilv_world, INGEN__Plugin); + LilvNodes* internals = lilv_world_find_nodes( + lilv_world, nullptr, rdf_type, ingen_Plugin); + LILV_FOREACH(nodes, i, internals) { + const LilvNode* internal = lilv_nodes_get(internals, i); + lilv_world_load_resource(lilv_world, internal); + } + lilv_nodes_free(internals); + lilv_node_free(rdf_type); + lilv_node_free(ingen_Plugin); + } + + ~Impl() + { + if (engine) { + engine->quit(); + } + + // Delete module objects but save pointers to libraries + typedef std::list<std::unique_ptr<Library>> Libs; + Libs libs; + for (auto& m : modules) { + libs.emplace_back(std::move(m.second->library)); + delete m.second; + } + + serialiser.reset(); + parser.reset(); + interface.reset(); + engine.reset(); + store.reset(); + + interface_factories.clear(); + script_runners.clear(); + + delete rdf_world; + delete lv2_features; + delete uris; + delete forge; + delete uri_map; + + lilv_world_free(lilv_world); + + // Module libraries go out of scope and close here + } + + typedef std::map<const std::string, Module*> Modules; + Modules modules; + + typedef std::map<const std::string, World::InterfaceFactory> InterfaceFactories; + InterfaceFactories interface_factories; + + typedef bool (*ScriptRunner)(World* world, const char* filename); + typedef std::map<const std::string, ScriptRunner> ScriptRunners; + ScriptRunners script_runners; + + int* argc; + char*** argv; + LV2Features* lv2_features; + Sord::World* rdf_world; + LilvWorld* lilv_world; + URIMap* uri_map; + Forge* forge; + URIs* uris; + LV2_Log_Log* lv2_log; + Configuration conf; + Log log; + SPtr<Interface> interface; + SPtr<EngineBase> engine; + SPtr<Serialiser> serialiser; + SPtr<Parser> parser; + SPtr<Store> store; + std::mutex rdf_mutex; + std::string jack_uuid; +}; + +World::World(LV2_URID_Map* map, LV2_URID_Unmap* unmap, LV2_Log_Log* log) + : _impl(new Impl(map, unmap, log)) +{ + _impl->serialiser = SPtr<Serialiser>(new Serialiser(*this)); + _impl->parser = SPtr<Parser>(new Parser()); +} + +World::~World() +{ + delete _impl; +} + +void +World::load_configuration(int& argc, char**& argv) +{ + _impl->argc = &argc; + _impl->argv = &argv; + + // Parse default configuration files + const auto files = _impl->conf.load_default("ingen", "options.ttl"); + for (const auto& f : files) { + _impl->log.info(fmt("Loaded configuration %1%\n") % f); + } + + // Parse command line options, overriding configuration file values + _impl->conf.parse(argc, argv); + _impl->log.set_flush(_impl->conf.option("flush-log").get<int32_t>()); + _impl->log.set_trace(_impl->conf.option("trace").get<int32_t>()); +} + +void World::set_engine(SPtr<EngineBase> e) { _impl->engine = e; } +void World::set_interface(SPtr<Interface> i) { _impl->interface = i; } +void World::set_store(SPtr<Store> s) { _impl->store = s; } + +SPtr<EngineBase> World::engine() { return _impl->engine; } +SPtr<Interface> World::interface() { return _impl->interface; } +SPtr<Parser> World::parser() { return _impl->parser; } +SPtr<Serialiser> World::serialiser() { return _impl->serialiser; } +SPtr<Store> World::store() { return _impl->store; } + +int& World::argc() { return *_impl->argc; } +char**& World::argv() { return *_impl->argv; } +Configuration& World::conf() { return _impl->conf; } +Log& World::log() { return _impl->log; } + +std::mutex& World::rdf_mutex() { return _impl->rdf_mutex; } + +Sord::World* World::rdf_world() { return _impl->rdf_world; } +LilvWorld* World::lilv_world() { return _impl->lilv_world; } + +LV2Features& World::lv2_features() { return *_impl->lv2_features; } +Forge& World::forge() { return *_impl->forge; } +URIs& World::uris() { return *_impl->uris; } +URIMap& World::uri_map() { return *_impl->uri_map; } + +bool +World::load_module(const char* name) +{ + auto i = _impl->modules.find(name); + if (i != _impl->modules.end()) { + return true; + } + log().info(fmt("Loading %1% module\n") % name); + std::unique_ptr<Ingen::Library> lib = ingen_load_library(log(), name); + Ingen::Module* (*module_load)() = + lib ? (Ingen::Module* (*)())lib->get_function("ingen_module_load") + : nullptr; + if (module_load) { + Module* module = module_load(); + if (module) { + module->library = std::move(lib); + module->load(this); + _impl->modules.emplace(string(name), module); + return true; + } + } + + log().error(fmt("Failed to load module `%1%' (%2%)\n") % name % lib->get_last_error()); + return false; +} + +bool +World::run_module(const char* name) +{ + auto i = _impl->modules.find(name); + if (i == _impl->modules.end()) { + log().error(fmt("Attempt to run unloaded module `%1%'\n") % name); + return false; + } + + i->second->run(this); + return true; +} + +/** Get an interface for a remote engine at `engine_uri` + */ +SPtr<Interface> +World::new_interface(const URI& engine_uri, SPtr<Interface> respondee) +{ + const Impl::InterfaceFactories::const_iterator i = + _impl->interface_factories.find(std::string(engine_uri.scheme())); + if (i == _impl->interface_factories.end()) { + log().warn(fmt("Unknown URI scheme `%1%'\n") % engine_uri.scheme()); + return SPtr<Interface>(); + } + + return i->second(this, engine_uri, respondee); +} + +/** Run a script of type `mime_type` at filename `filename` */ +bool +World::run(const std::string& mime_type, const std::string& filename) +{ + const Impl::ScriptRunners::const_iterator i = _impl->script_runners.find(mime_type); + if (i == _impl->script_runners.end()) { + log().warn(fmt("Unknown script MIME type `%1%'\n") % mime_type); + return false; + } + + return i->second(this, filename.c_str()); +} + +void +World::add_interface_factory(const std::string& scheme, InterfaceFactory factory) +{ + _impl->interface_factories.emplace(scheme, factory); +} + +void +World::set_jack_uuid(const std::string& uuid) +{ + _impl->jack_uuid = uuid; +} + +std::string +World::jack_uuid() +{ + return _impl->jack_uuid; +} + +} // namespace Ingen diff --git a/src/client/BlockModel.cpp b/src/client/BlockModel.cpp new file mode 100644 index 00000000..910f7037 --- /dev/null +++ b/src/client/BlockModel.cpp @@ -0,0 +1,285 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cmath> +#include <string> + +#include "ingen/client/BlockModel.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" + +namespace Ingen { +namespace Client { + +BlockModel::BlockModel(URIs& uris, + SPtr<PluginModel> plugin, + const Raul::Path& path) + : ObjectModel(uris, path) + , _plugin_uri(plugin->uri()) + , _plugin(plugin) + , _num_values(0) + , _min_values(nullptr) + , _max_values(nullptr) +{ +} + +BlockModel::BlockModel(URIs& uris, + const URI& plugin_uri, + const Raul::Path& path) + : ObjectModel(uris, path) + , _plugin_uri(plugin_uri) + , _num_values(0) + , _min_values(nullptr) + , _max_values(nullptr) +{ +} + +BlockModel::BlockModel(const BlockModel& copy) + : ObjectModel(copy) + , _plugin_uri(copy._plugin_uri) + , _num_values(copy._num_values) + , _min_values((float*)malloc(sizeof(float) * _num_values)) + , _max_values((float*)malloc(sizeof(float) * _num_values)) +{ + memcpy(_min_values, copy._min_values, sizeof(float) * _num_values); + memcpy(_max_values, copy._max_values, sizeof(float) * _num_values); +} + +BlockModel::~BlockModel() +{ + clear(); +} + +void +BlockModel::remove_port(SPtr<PortModel> port) +{ + for (auto i = _ports.begin(); i != _ports.end(); ++i) { + if ((*i) == port) { + _ports.erase(i); + break; + } + } + _signal_removed_port.emit(port); +} + +void +BlockModel::remove_port(const Raul::Path& port_path) +{ + for (auto i = _ports.begin(); i != _ports.end(); ++i) { + if ((*i)->path() == port_path) { + _ports.erase(i); + break; + } + } +} + +void +BlockModel::clear() +{ + _ports.clear(); + assert(_ports.empty()); + delete[] _min_values; + delete[] _max_values; + _min_values = nullptr; + _max_values = nullptr; +} + +void +BlockModel::add_child(SPtr<ObjectModel> c) +{ + assert(c->parent().get() == this); + + //ObjectModel::add_child(c); + + SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(c); + assert(pm); + add_port(pm); +} + +bool +BlockModel::remove_child(SPtr<ObjectModel> c) +{ + assert(c->path().is_child_of(path())); + assert(c->parent().get() == this); + + //bool ret = ObjectModel::remove_child(c); + + SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(c); + assert(pm); + remove_port(pm); + + //return ret; + return true; +} + +void +BlockModel::add_port(SPtr<PortModel> pm) +{ + assert(pm); + assert(pm->path().is_child_of(path())); + assert(pm->parent().get() == this); + + // Store should have handled this by merging the two + assert(find(_ports.begin(), _ports.end(), pm) == _ports.end()); + + _ports.push_back(pm); + _signal_new_port.emit(pm); +} + +SPtr<const PortModel> +BlockModel::get_port(const Raul::Symbol& symbol) const +{ + for (auto p : _ports) { + if (p->symbol() == symbol) { + return p; + } + } + return SPtr<PortModel>(); +} + +SPtr<const PortModel> +BlockModel::get_port(uint32_t index) const +{ + return _ports[index]; +} + +Ingen::Node* +BlockModel::port(uint32_t index) const +{ + assert(index < num_ports()); + return const_cast<Ingen::Node*>( + dynamic_cast<const Ingen::Node*>(_ports[index].get())); +} + +void +BlockModel::default_port_value_range(SPtr<const PortModel> port, + float& min, + float& max, + uint32_t srate) const +{ + // Default control values + min = 0.0; + max = 1.0; + + // Get range from client-side LV2 data + if (_plugin && _plugin->lilv_plugin()) { + if (!_min_values) { + _num_values = lilv_plugin_get_num_ports(_plugin->lilv_plugin()); + _min_values = new float[_num_values]; + _max_values = new float[_num_values]; + lilv_plugin_get_port_ranges_float(_plugin->lilv_plugin(), + _min_values, _max_values, nullptr); + } + + if (!std::isnan(_min_values[port->index()])) { + min = _min_values[port->index()]; + } + if (!std::isnan(_max_values[port->index()])) { + max = _max_values[port->index()]; + } + } + + if (port->port_property(_uris.lv2_sampleRate)) { + min *= srate; + max *= srate; + } +} + +void +BlockModel::port_value_range(SPtr<const PortModel> port, + float& min, + float& max, + uint32_t srate) const +{ + assert(port->parent().get() == this); + + default_port_value_range(port, min, max); + + // Possibly overriden + const Atom& min_atom = port->get_property(_uris.lv2_minimum); + const Atom& max_atom = port->get_property(_uris.lv2_maximum); + if (min_atom.type() == _uris.forge.Float) { + min = min_atom.get<float>(); + } + if (max_atom.type() == _uris.forge.Float) { + max = max_atom.get<float>(); + } + + if (max <= min) { + max = min + 1.0; + } + + if (port->port_property(_uris.lv2_sampleRate)) { + min *= srate; + max *= srate; + } +} + +std::string +BlockModel::label() const +{ + const Atom& name_property = get_property(_uris.lv2_name); + if (name_property.type() == _uris.forge.String) { + return name_property.ptr<char>(); + } else if (plugin_model()) { + return plugin_model()->human_name(); + } else { + return symbol().c_str(); + } +} + +std::string +BlockModel::port_label(SPtr<const PortModel> port) const +{ + const Atom& name = port->get_property(URI(LV2_CORE__name)); + if (name.is_valid() && name.type() == _uris.forge.String) { + return name.ptr<char>(); + } + + if (_plugin && _plugin->lilv_plugin()) { + LilvWorld* w = _plugin->lilv_world(); + const LilvPlugin* plug = _plugin->lilv_plugin(); + LilvNode* sym = lilv_new_string(w, port->symbol().c_str()); + const LilvPort* lport = lilv_plugin_get_port_by_symbol(plug, sym); + if (lport) { + LilvNode* lname = lilv_port_get_name(plug, lport); + if (lname && lilv_node_is_string(lname)) { + std::string ret(lilv_node_as_string(lname)); + lilv_node_free(lname); + return ret; + } + lilv_node_free(lname); + } + } + + return port->symbol().c_str(); +} + +void +BlockModel::set(SPtr<ObjectModel> model) +{ + SPtr<BlockModel> block = dynamic_ptr_cast<BlockModel>(model); + if (block) { + _plugin_uri = block->_plugin_uri; + _plugin = block->_plugin; + } + + ObjectModel::set(model); +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/ClientStore.cpp b/src/client/ClientStore.cpp new file mode 100644 index 00000000..792f8949 --- /dev/null +++ b/src/client/ClientStore.cpp @@ -0,0 +1,487 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <boost/variant/apply_visitor.hpp> + +#include "ingen/Log.hpp" +#include "ingen/client/ArcModel.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/ObjectModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/client/PortModel.hpp" +#include "ingen/client/SigClientInterface.hpp" + +namespace Ingen { +namespace Client { + +ClientStore::ClientStore(URIs& uris, + Log& log, + SPtr<SigClientInterface> emitter) + : _uris(uris) + , _log(log) + , _emitter(emitter) + , _plugins(new Plugins()) +{ + if (emitter) { + emitter->signal_message().connect( + sigc::mem_fun(this, &ClientStore::message)); + } +} + +void +ClientStore::clear() +{ + Store::clear(); + _plugins->clear(); +} + +void +ClientStore::add_object(SPtr<ObjectModel> object) +{ + // If we already have "this" object, merge the existing one into the new + // one (with precedence to the new values). + auto existing = find(object->path()); + if (existing != end()) { + dynamic_ptr_cast<ObjectModel>(existing->second)->set(object); + } else { + if (!object->path().is_root()) { + SPtr<ObjectModel> parent = _object(object->path().parent()); + if (parent) { + assert(object->path().is_child_of(parent->path())); + object->set_parent(parent); + parent->add_child(object); + assert(parent && (object->parent() == parent)); + + (*this)[object->path()] = object; + _signal_new_object.emit(object); + } else { + _log.error(fmt("Object %1% with no parent\n") % object->path()); + } + } else { + (*this)[object->path()] = object; + _signal_new_object.emit(object); + } + } + + for (auto p : object->properties()) { + object->signal_property().emit(p.first, p.second); + } +} + +SPtr<ObjectModel> +ClientStore::remove_object(const Raul::Path& path) +{ + // Find the object, the "top" of the tree to remove + const iterator top = find(path); + if (top == end()) { + return SPtr<ObjectModel>(); + } + + SPtr<ObjectModel> object = dynamic_ptr_cast<ObjectModel>(top->second); + + // Remove object and any adjacent arcs from parent if applicable + if (object && object->parent()) { + SPtr<PortModel> port = dynamic_ptr_cast<PortModel>(object); + if (port && dynamic_ptr_cast<GraphModel>(port->parent())) { + disconnect_all(port->parent()->path(), path); + if (port->parent()->parent()) { + disconnect_all(port->parent()->parent()->path(), path); + } + } else if (port && port->parent()->parent()) { + disconnect_all(port->parent()->parent()->path(), path); + } else { + disconnect_all(object->parent()->path(), path); + } + + object->parent()->remove_child(object); + } + + // Remove the object and all its descendants + Objects removed; + remove(top, removed); + + // Notify everything that needs to know this object has been removed + if (object) { + object->signal_destroyed().emit(); + } + + return object; +} + +SPtr<PluginModel> +ClientStore::_plugin(const URI& uri) +{ + const Plugins::iterator i = _plugins->find(uri); + return (i == _plugins->end()) ? SPtr<PluginModel>() : (*i).second; +} + +SPtr<PluginModel> +ClientStore::_plugin(const Atom& uri) +{ + /* FIXME: Should probably be stored with URIs rather than strings, to make + this a fast case. */ + + const Plugins::iterator i = _plugins->find(URI(_uris.forge.str(uri, false))); + return (i == _plugins->end()) ? SPtr<PluginModel>() : (*i).second; +} + +SPtr<const PluginModel> +ClientStore::plugin(const URI& uri) const +{ + return const_cast<ClientStore*>(this)->_plugin(uri); +} + +SPtr<ObjectModel> +ClientStore::_object(const Raul::Path& path) +{ + const iterator i = find(path); + if (i == end()) { + return SPtr<ObjectModel>(); + } else { + SPtr<ObjectModel> model = dynamic_ptr_cast<ObjectModel>(i->second); + assert(model); + assert(model->path().is_root() || model->parent()); + return model; + } +} + +SPtr<const ObjectModel> +ClientStore::object(const Raul::Path& path) const +{ + return const_cast<ClientStore*>(this)->_object(path); +} + +SPtr<Resource> +ClientStore::_resource(const URI& uri) +{ + if (uri_is_path(uri)) { + return _object(uri_to_path(uri)); + } else { + return _plugin(uri); + } +} + +SPtr<const Resource> +ClientStore::resource(const URI& uri) const +{ + return const_cast<ClientStore*>(this)->_resource(uri); +} + +void +ClientStore::add_plugin(SPtr<PluginModel> pm) +{ + SPtr<PluginModel> existing = _plugin(pm->uri()); + if (existing) { + existing->set(pm); + } else { + _plugins->emplace(pm->uri(), pm); + _signal_new_plugin.emit(pm); + } +} + +/* ****** Signal Handlers ******** */ + +void +ClientStore::operator()(const Del& del) +{ + if (uri_is_path(del.uri)) { + remove_object(uri_to_path(del.uri)); + } else { + auto p = _plugins->find(del.uri); + if (p != _plugins->end()) { + _plugins->erase(p); + _signal_plugin_deleted.emit(del.uri); + } + } +} + +void +ClientStore::operator()(const Copy&) +{ + _log.error("Client store copy unsupported\n"); +} + +void +ClientStore::operator()(const Move& msg) +{ + const iterator top = find(msg.old_path); + if (top != end()) { + rename(top, msg.new_path); + } +} + +void +ClientStore::message(const Message& msg) +{ + boost::apply_visitor(*this, msg); +} + +void +ClientStore::operator()(const Put& msg) +{ + typedef Properties::const_iterator Iterator; + + const auto& uri = msg.uri; + const auto& properties = msg.properties; + + bool is_graph, is_block, is_port, is_output; + Resource::type(uris(), properties, + is_graph, is_block, is_port, is_output); + + // Check for specially handled types + const Iterator t = properties.find(_uris.rdf_type); + if (t != properties.end()) { + const Atom& type(t->second); + if (_uris.pset_Preset == type) { + const Iterator p = properties.find(_uris.lv2_appliesTo); + const Iterator l = properties.find(_uris.rdfs_label); + SPtr<PluginModel> plug; + if (p == properties.end()) { + _log.error(fmt("Preset <%1%> with no plugin\n") % uri.c_str()); + } else if (l == properties.end()) { + _log.error(fmt("Preset <%1%> with no label\n") % uri.c_str()); + } else if (l->second.type() != _uris.forge.String) { + _log.error(fmt("Preset <%1%> label is not a string\n") % uri.c_str()); + } else if (!(plug = _plugin(p->second))) { + _log.error(fmt("Preset <%1%> for unknown plugin %2%\n") + % uri.c_str() % _uris.forge.str(p->second, true)); + } else { + plug->add_preset(uri, l->second.ptr<char>()); + } + return; + } else if (_uris.ingen_Graph == type) { + is_graph = true; + } else if (_uris.ingen_Internal == type || _uris.lv2_Plugin == type) { + SPtr<PluginModel> p(new PluginModel(uris(), uri, type, properties)); + add_plugin(p); + return; + } + } + + if (!uri_is_path(uri)) { + _log.error(fmt("Put for unknown subject <%1%>\n") + % uri.c_str()); + return; + } + + const Raul::Path path(uri_to_path(uri)); + + SPtr<ObjectModel> obj = dynamic_ptr_cast<ObjectModel>(_object(path)); + if (obj) { + obj->set_properties(properties); + return; + } + + if (path.is_root()) { + is_graph = true; + } + + if (is_graph) { + SPtr<GraphModel> model(new GraphModel(uris(), path)); + model->set_properties(properties); + add_object(model); + } else if (is_block) { + auto p = properties.find(_uris.lv2_prototype); + if (p == properties.end()) { + p = properties.find(_uris.ingen_prototype); + } + + SPtr<PluginModel> plug; + if (p->second.is_valid() && (p->second.type() == _uris.forge.URI || + p->second.type() == _uris.forge.URID)) { + const URI uri(_uris.forge.str(p->second, false)); + if (!(plug = _plugin(uri))) { + plug = SPtr<PluginModel>( + new PluginModel(uris(), uri, Atom(), Properties())); + add_plugin(plug); + } + + SPtr<BlockModel> bm(new BlockModel(uris(), plug, path)); + bm->set_properties(properties); + add_object(bm); + } else { + _log.warn(fmt("Block %1% has no prototype\n") % path.c_str()); + } + } else if (is_port) { + PortModel::Direction pdir = (is_output) + ? PortModel::Direction::OUTPUT + : PortModel::Direction::INPUT; + uint32_t index = 0; + const Iterator i = properties.find(_uris.lv2_index); + if (i != properties.end() && i->second.type() == _uris.forge.Int) { + index = i->second.get<int32_t>(); + } + + SPtr<PortModel> p(new PortModel(uris(), path, index, pdir)); + p->set_properties(properties); + add_object(p); + } else { + _log.warn(fmt("Ignoring %1% of unknown type\n") % path.c_str()); + } +} + +void +ClientStore::operator()(const Delta& msg) +{ + const auto& uri = msg.uri; + if (uri == URI("ingen:/clients/this")) { + // Client property, which we don't store (yet?) + return; + } + + if (!uri_is_path(uri)) { + _log.error(fmt("Delta for unknown subject <%1%>\n") + % uri.c_str()); + return; + } + + const Raul::Path path(uri_to_path(uri)); + + SPtr<ObjectModel> obj = _object(path); + if (obj) { + obj->remove_properties(msg.remove); + obj->add_properties(msg.add); + } else { + _log.warn(fmt("Failed to find object `%1%'\n") % path.c_str()); + } +} + +void +ClientStore::operator()(const SetProperty& msg) +{ + const auto& subject_uri = msg.subject; + const auto& predicate = msg.predicate; + const auto& value = msg.value; + + if (subject_uri == URI("ingen:/engine")) { + _log.info(fmt("Engine property <%1%> = %2%\n") + % predicate.c_str() % _uris.forge.str(value, false)); + return; + } + SPtr<Resource> subject = _resource(subject_uri); + if (subject) { + if (predicate == _uris.ingen_activity) { + /* Activity is transient, trigger any live actions (like GUI + blinkenlights) but do not store the property. */ + subject->on_property(predicate, value); + } else { + subject->set_property(predicate, value, msg.ctx); + } + } else { + SPtr<PluginModel> plugin = _plugin(subject_uri); + if (plugin) { + plugin->set_property(predicate, value); + } else if (predicate != _uris.ingen_activity) { + _log.warn(fmt("Property <%1%> for unknown object %2%\n") + % predicate.c_str() % subject_uri.c_str()); + } + } +} + +SPtr<GraphModel> +ClientStore::connection_graph(const Raul::Path& tail_path, + const Raul::Path& head_path) +{ + SPtr<GraphModel> graph; + + if (tail_path.parent() == head_path.parent()) { + graph = dynamic_ptr_cast<GraphModel>(_object(tail_path.parent())); + } + + if (!graph && tail_path.parent() == head_path.parent().parent()) { + graph = dynamic_ptr_cast<GraphModel>(_object(tail_path.parent())); + } + + if (!graph && tail_path.parent().parent() == head_path.parent()) { + graph = dynamic_ptr_cast<GraphModel>(_object(head_path.parent())); + } + + if (!graph) { + graph = dynamic_ptr_cast<GraphModel>(_object(tail_path.parent().parent())); + } + + if (!graph) { + _log.error(fmt("Unable to find graph for arc %1% => %2%\n") + % tail_path % head_path); + } + + return graph; +} + +bool +ClientStore::attempt_connection(const Raul::Path& tail_path, + const Raul::Path& head_path) +{ + SPtr<PortModel> tail = dynamic_ptr_cast<PortModel>(_object(tail_path)); + SPtr<PortModel> head = dynamic_ptr_cast<PortModel>(_object(head_path)); + + if (tail && head) { + SPtr<GraphModel> graph = connection_graph(tail_path, head_path); + SPtr<ArcModel> arc(new ArcModel(tail, head)); + graph->add_arc(arc); + return true; + } else { + _log.warn(fmt("Failed to connect %1% => %2%\n") + % tail_path % head_path); + return false; + } +} + +void +ClientStore::operator()(const Connect& msg) +{ + attempt_connection(msg.tail, msg.head); +} + +void +ClientStore::operator()(const Disconnect& msg) +{ + SPtr<PortModel> tail = dynamic_ptr_cast<PortModel>(_object(msg.tail)); + SPtr<PortModel> head = dynamic_ptr_cast<PortModel>(_object(msg.head)); + SPtr<GraphModel> graph = connection_graph(msg.tail, msg.head); + if (graph) { + graph->remove_arc(tail.get(), head.get()); + } +} + +void +ClientStore::operator()(const DisconnectAll& msg) +{ + SPtr<GraphModel> graph = dynamic_ptr_cast<GraphModel>(_object(msg.graph)); + SPtr<ObjectModel> object = _object(msg.path); + + if (!graph || !object) { + _log.error(fmt("Bad disconnect all notification %1% in %2%\n") + % msg.path % msg.graph); + return; + } + + const GraphModel::Arcs arcs = graph->arcs(); + for (auto a : arcs) { + SPtr<ArcModel> arc = dynamic_ptr_cast<ArcModel>(a.second); + if (arc->tail()->parent() == object + || arc->head()->parent() == object + || arc->tail()->path() == msg.path + || arc->head()->path() == msg.path) { + graph->remove_arc(arc->tail().get(), arc->head().get()); + } + } +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/GraphModel.cpp b/src/client/GraphModel.cpp new file mode 100644 index 00000000..0723e59b --- /dev/null +++ b/src/client/GraphModel.cpp @@ -0,0 +1,176 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> + +#include "ingen/URIs.hpp" +#include "ingen/client/ArcModel.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +namespace Ingen { +namespace Client { + +void +GraphModel::add_child(SPtr<ObjectModel> c) +{ + assert(c->parent().get() == this); + + SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(c); + if (pm) { + add_port(pm); + return; + } + + SPtr<BlockModel> bm = dynamic_ptr_cast<BlockModel>(c); + if (bm) { + _signal_new_block.emit(bm); + } +} + +bool +GraphModel::remove_child(SPtr<ObjectModel> o) +{ + assert(o->path().is_child_of(path())); + assert(o->parent().get() == this); + + SPtr<PortModel> pm = dynamic_ptr_cast<PortModel>(o); + if (pm) { + remove_arcs_on(pm); + remove_port(pm); + } + + SPtr<BlockModel> bm = dynamic_ptr_cast<BlockModel>(o); + if (bm) { + _signal_removed_block.emit(bm); + } + + return true; +} + +void +GraphModel::remove_arcs_on(SPtr<PortModel> p) +{ + // Remove any connections which referred to this object, + // since they can't possibly exist anymore + for (auto j = _arcs.begin(); j != _arcs.end();) { + auto next = j; + ++next; + + SPtr<ArcModel> arc = dynamic_ptr_cast<ArcModel>(j->second); + if (arc->tail_path().parent() == p->path() + || arc->tail_path() == p->path() + || arc->head_path().parent() == p->path() + || arc->head_path() == p->path()) { + _signal_removed_arc.emit(arc); + _arcs.erase(j); // Cuts our reference + } + j = next; + } +} + +void +GraphModel::clear() +{ + _arcs.clear(); + + BlockModel::clear(); + + assert(_arcs.empty()); + assert(_ports.empty()); +} + +SPtr<ArcModel> +GraphModel::get_arc(const Node* tail, const Node* head) +{ + auto i = _arcs.find(std::make_pair(tail, head)); + if (i != _arcs.end()) { + return dynamic_ptr_cast<ArcModel>(i->second); + } else { + return SPtr<ArcModel>(); + } +} + +/** Add a connection to this graph. + * + * A reference to `arc` is taken, released on deletion or removal. + * If `arc` only contains paths (not pointers to the actual ports), the ports + * will be found and set. The ports referred to not existing as children of + * this graph is a fatal error. + */ +void +GraphModel::add_arc(SPtr<ArcModel> arc) +{ + // Store should have 'resolved' the connection already + assert(arc); + assert(arc->tail()); + assert(arc->head()); + assert(arc->tail()->parent()); + assert(arc->head()->parent()); + assert(arc->tail_path() != arc->head_path()); + assert(arc->tail()->parent().get() == this + || arc->tail()->parent()->parent().get() == this); + assert(arc->head()->parent().get() == this + || arc->head()->parent()->parent().get() == this); + + SPtr<ArcModel> existing = get_arc( + arc->tail().get(), arc->head().get()); + + if (existing) { + assert(arc->tail() == existing->tail()); + assert(arc->head() == existing->head()); + } else { + _arcs.emplace(std::make_pair(arc->tail().get(), arc->head().get()), + arc); + _signal_new_arc.emit(arc); + } +} + +void +GraphModel::remove_arc(const Node* tail, const Node* head) +{ + auto i = _arcs.find(std::make_pair(tail, head)); + if (i != _arcs.end()) { + SPtr<ArcModel> arc = dynamic_ptr_cast<ArcModel>(i->second); + _signal_removed_arc.emit(arc); + _arcs.erase(i); + } +} + +bool +GraphModel::enabled() const +{ + const Atom& enabled = get_property(_uris.ingen_enabled); + return (enabled.is_valid() && enabled.get<int32_t>()); +} + +uint32_t +GraphModel::internal_poly() const +{ + const Atom& poly = get_property(_uris.ingen_polyphony); + return poly.is_valid() ? poly.get<int32_t>() : 1; +} + +bool +GraphModel::polyphonic() const +{ + const Atom& poly = get_property(_uris.ingen_polyphonic); + return poly.is_valid() && poly.get<int32_t>(); +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/ObjectModel.cpp b/src/client/ObjectModel.cpp new file mode 100644 index 00000000..8d40b120 --- /dev/null +++ b/src/client/ObjectModel.cpp @@ -0,0 +1,108 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Node.hpp" +#include "ingen/URIs.hpp" +#include "ingen/client/ObjectModel.hpp" + +namespace Ingen { +namespace Client { + +ObjectModel::ObjectModel(URIs& uris, const Raul::Path& path) + : Node(uris, path) + , _path(path) + , _symbol((path == "/") ? "root" : path.symbol()) +{ +} + +ObjectModel::ObjectModel(const ObjectModel& copy) + : Node(copy) + , _parent(copy._parent) + , _path(copy._path) + , _symbol(copy._symbol) +{ +} + +bool +ObjectModel::is_a(const URIs::Quark& type) const +{ + return has_property(_uris.rdf_type, type); +} + +void +ObjectModel::on_property(const URI& uri, const Atom& value) +{ + _signal_property.emit(uri, value); +} + +void +ObjectModel::on_property_removed(const URI& uri, const Atom& value) +{ + _signal_property_removed.emit(uri, value); +} + +const Atom& +ObjectModel::get_property(const URI& key) const +{ + static const Atom null_atom; + auto i = properties().find(key); + return (i != properties().end()) ? i->second : null_atom; +} + +bool +ObjectModel::polyphonic() const +{ + const Atom& polyphonic = get_property(_uris.ingen_polyphonic); + return (polyphonic.is_valid() && polyphonic.get<int32_t>()); +} + +/** Merge the data of `o` with self, as much as possible. + * + * This will merge the two models, but with any conflict take the value in + * `o` as correct. The paths of the two models MUST be equal. + */ +void +ObjectModel::set(SPtr<ObjectModel> o) +{ + assert(_path == o->path()); + if (o->_parent) { + _parent = o->_parent; + } + + for (auto v : o->properties()) { + Resource::set_property(v.first, v.second); + _signal_property.emit(v.first, v.second); + } +} + +void +ObjectModel::set_path(const Raul::Path& p) +{ + _path = p; + _symbol = Raul::Symbol(p.is_root() ? "root" : p.symbol()); + set_uri(path_to_uri(p)); + _signal_moved.emit(); +} + +void +ObjectModel::set_parent(SPtr<ObjectModel> p) +{ + assert(_path.is_child_of(p->path())); + _parent = p; +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/PluginModel.cpp b/src/client/PluginModel.cpp new file mode 100644 index 00000000..5427b75e --- /dev/null +++ b/src/client/PluginModel.cpp @@ -0,0 +1,360 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> +#include <algorithm> + +#include <boost/optional.hpp> + +#include "raul/Path.hpp" + +#include "ingen/Atom.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/client/PluginUI.hpp" + +#include "ingen_config.h" + +using std::string; + +namespace Ingen { +namespace Client { + +LilvWorld* PluginModel::_lilv_world = nullptr; +const LilvPlugins* PluginModel::_lilv_plugins = nullptr; + +Sord::World* PluginModel::_rdf_world = nullptr; + +PluginModel::PluginModel(URIs& uris, + const URI& uri, + const Atom& type, + const Properties& properties) + : Resource(uris, uri) + , _type(type) + , _fetched(false) +{ + if (!_type.is_valid()) { + if (uri.string().find("ingen-internals") != string::npos) { + _type = uris.ingen_Internal.urid; + } else { + _type = uris.lv2_Plugin.urid; // Assume LV2 and hope for the best... + } + } + + add_property(uris.rdf_type, type); + add_properties(properties); + + LilvNode* plugin_uri = lilv_new_uri(_lilv_world, uri.c_str()); + _lilv_plugin = lilv_plugins_get_by_uri(_lilv_plugins, plugin_uri); + lilv_node_free(plugin_uri); + + if (uris.ingen_Internal == _type) { + set_property(uris.doap_name, + uris.forge.alloc(std::string(uri.fragment().substr(1)))); + } +} + +static size_t +last_uri_delim(const std::string& str) +{ + for (size_t i = str.length() - 1; i > 0; --i) { + switch (str[i]) { + case ':': case '/': case '?': case '#': + return i; + } + } + return string::npos; +} + +static bool +contains_alpha_after(const std::string& str, size_t begin) +{ + for (size_t i = begin; i < str.length(); ++i) { + if (isalpha(str[i])) { + return true; + } + } + return false; +} + +const Atom& +PluginModel::get_property(const URI& key) const +{ + static const Atom nil; + const Atom& val = Resource::get_property(key); + if (val.is_valid()) { + return val; + } + + // No lv2:symbol from data or engine, invent one + if (key == _uris.lv2_symbol) { + string str = this->uri(); + size_t last_delim = last_uri_delim(str); + while (last_delim != string::npos && + !contains_alpha_after(str, last_delim)) { + str = str.substr(0, last_delim); + last_delim = last_uri_delim(str); + } + str = str.substr(last_delim + 1); + + std::string symbol = Raul::Symbol::symbolify(str); + set_property(_uris.lv2_symbol, _uris.forge.alloc(symbol)); + return get_property(key); + } + + if (_lilv_plugin) { + boost::optional<const Atom&> ret; + LilvNode* lv2_pred = lilv_new_uri(_lilv_world, key.c_str()); + LilvNodes* values = lilv_plugin_get_value(_lilv_plugin, lv2_pred); + lilv_node_free(lv2_pred); + LILV_FOREACH(nodes, i, values) { + const LilvNode* val = lilv_nodes_get(values, i); + if (lilv_node_is_uri(val)) { + ret = set_property( + key, _uris.forge.make_urid(URI(lilv_node_as_uri(val)))); + break; + } else if (lilv_node_is_string(val)) { + ret = set_property( + key, _uris.forge.alloc(lilv_node_as_string(val))); + break; + } else if (lilv_node_is_float(val)) { + ret = set_property( + key, _uris.forge.make(lilv_node_as_float(val))); + break; + } else if (lilv_node_is_int(val)) { + ret = set_property( + key, _uris.forge.make(lilv_node_as_int(val))); + break; + } + } + lilv_nodes_free(values); + + if (ret) { + return *ret; + } + } + + return nil; +} + +void +PluginModel::set(SPtr<PluginModel> p) +{ + _type = p->_type; + + if (p->_lilv_plugin) { + _lilv_plugin = p->_lilv_plugin; + } + + for (auto v : p->properties()) { + Resource::set_property(v.first, v.second); + _signal_property.emit(v.first, v.second); + } + + _signal_changed.emit(); +} + +void +PluginModel::add_preset(const URI& uri, const std::string& label) +{ + _presets.emplace(uri, label); + _signal_preset.emit(uri, label); +} + +Raul::Symbol +PluginModel::default_block_symbol() const +{ + const Atom& name_atom = get_property(_uris.lv2_symbol); + if (name_atom.is_valid() && name_atom.type() == _uris.forge.String) { + return Raul::Symbol::symbolify(name_atom.ptr<char>()); + } else { + return Raul::Symbol("_"); + } +} + +string +PluginModel::human_name() const +{ + const Atom& name_atom = get_property(_uris.doap_name); + if (name_atom.type() == _uris.forge.String) { + return name_atom.ptr<char>(); + } else { + return default_block_symbol().c_str(); + } +} + +string +PluginModel::port_human_name(uint32_t i) const +{ + if (_lilv_plugin) { + const LilvPort* port = lilv_plugin_get_port_by_index(_lilv_plugin, i); + LilvNode* name = lilv_port_get_name(_lilv_plugin, port); + const string ret(lilv_node_as_string(name)); + lilv_node_free(name); + return ret; + } + return ""; +} + +PluginModel::ScalePoints +PluginModel::port_scale_points(uint32_t i) const +{ + // TODO: Non-float scale points + ScalePoints points; + if (_lilv_plugin) { + const LilvPort* port = lilv_plugin_get_port_by_index(_lilv_plugin, i); + LilvScalePoints* sp = lilv_port_get_scale_points(_lilv_plugin, port); + LILV_FOREACH(scale_points, i, sp) { + const LilvScalePoint* p = lilv_scale_points_get(sp, i); + points.emplace( + lilv_node_as_float(lilv_scale_point_get_value(p)), + lilv_node_as_string(lilv_scale_point_get_label(p))); + } + } + return points; +} + +bool +PluginModel::has_ui() const +{ + if (_lilv_plugin) { + LilvUIs* uis = lilv_plugin_get_uis(_lilv_plugin); + const bool ret = (lilv_nodes_size(uis) > 0); + lilv_uis_free(uis); + return ret; + } + return false; +} + +SPtr<PluginUI> +PluginModel::ui(Ingen::World* world, + SPtr<const BlockModel> block) const +{ + if (!_lilv_plugin) { + return SPtr<PluginUI>(); + } + + return PluginUI::create(world, block, _lilv_plugin); +} + +static std::string +heading(const std::string& text, bool html, unsigned level) +{ + if (html) { + const std::string tag = std::string("h") + std::to_string(level); + return std::string("<") + tag + ">" + text + "</" + tag + ">\n"; + } else { + return text + ":\n\n"; + } +} + +static std::string +link(const std::string& addr, bool html) +{ + if (html) { + return std::string("<a href=\"") + addr + "\">" + addr + "</a>"; + } else { + return addr; + } +} + +std::string +PluginModel::get_documentation(const LilvNode* subject, bool html) const +{ + std::string doc; + + LilvNode* lv2_documentation = lilv_new_uri(_lilv_world, + LV2_CORE__documentation); + LilvNode* rdfs_comment = lilv_new_uri(_lilv_world, + LILV_NS_RDFS "comment"); + + LilvNodes* vals = lilv_world_find_nodes( + _lilv_world, subject, lv2_documentation, nullptr); + const bool doc_is_html = vals; + if (!vals) { + vals = lilv_world_find_nodes( + _lilv_world, subject, rdfs_comment, nullptr); + } + + if (vals) { + const LilvNode* val = lilv_nodes_get_first(vals); + if (lilv_node_is_string(val)) { + doc += lilv_node_as_string(val); + } + } + + if (html && !doc_is_html) { + for (std::size_t i = 0; i < doc.size(); ++i) { + if (doc.substr(i, 2) == "\n\n") { + doc.replace(i, 2, "<br/><br/>"); + i += strlen("<br/><br/>"); + } + } + } + + lilv_node_free(rdfs_comment); + lilv_node_free(lv2_documentation); + + return doc; +} + +std::string +PluginModel::documentation(const bool html) const +{ + LilvNode* subject = (_lilv_plugin) + ? lilv_node_duplicate(lilv_plugin_get_uri(_lilv_plugin)) + : lilv_new_uri(_lilv_world, uri().c_str()); + + const std::string doc(get_documentation(subject, html)); + + lilv_node_free(subject); + + return (heading(human_name(), html, 2) + + link(uri(), html) + (html ? "<br/><br/>" : "\n\n") + + doc); +} + +std::string +PluginModel::port_documentation(uint32_t index, bool html) const +{ + if (!_lilv_plugin) { + return ""; + } + + const LilvPort* port = lilv_plugin_get_port_by_index(_lilv_plugin, index); + if (!port) { + return ""; + } + + return (heading(port_human_name(index), html, 2) + + get_documentation(lilv_port_get_node(_lilv_plugin, port), html)); +} + +const LilvPort* +PluginModel::lilv_port(uint32_t index) const +{ + return lilv_plugin_get_port_by_index(_lilv_plugin, index); +} + +void +PluginModel::set_lilv_world(LilvWorld* world) +{ + _lilv_world = world; + _lilv_plugins = lilv_world_get_all_plugins(_lilv_world); +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/PluginUI.cpp b/src/client/PluginUI.cpp new file mode 100644 index 00000000..df983f7f --- /dev/null +++ b/src/client/PluginUI.cpp @@ -0,0 +1,336 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/PluginUI.hpp" +#include "ingen/client/PortModel.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/extensions/ui/ui.h" + +namespace Ingen { +namespace Client { + +SuilHost* PluginUI::ui_host = nullptr; + +static SPtr<const PortModel> +get_port(PluginUI* ui, uint32_t port_index) +{ + if (port_index >= ui->block()->ports().size()) { + ui->world()->log().error( + fmt("%1% UI tried to access invalid port %2%\n") + % ui->block()->plugin()->uri().c_str() % port_index); + return SPtr<const PortModel>(); + } + return ui->block()->ports()[port_index]; +} + +static void +lv2_ui_write(SuilController controller, + uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void* buffer) +{ + PluginUI* const ui = (PluginUI*)controller; + const URIs& uris = ui->world()->uris(); + SPtr<const PortModel> port = get_port(ui, port_index); + if (!port) { + return; + } + + // float (special case, always 0) + if (format == 0) { + if (buffer_size != 4) { + ui->world()->log().error( + fmt("%1% UI wrote corrupt float with bad size\n") + % ui->block()->plugin()->uri().c_str()); + return; + } + const float value = *(const float*)buffer; + if (port->value().type() == uris.atom_Float && + value == port->value().get<float>()) { + return; // Ignore feedback + } + + ui->signal_property_changed()( + port->uri(), + uris.ingen_value, + ui->world()->forge().make(value), + Resource::Graph::DEFAULT); + + } else if (format == uris.atom_eventTransfer.urid.get<LV2_URID>()) { + const LV2_Atom* atom = (const LV2_Atom*)buffer; + Atom val = ui->world()->forge().alloc( + atom->size, atom->type, LV2_ATOM_BODY_CONST(atom)); + ui->signal_property_changed()(port->uri(), + uris.ingen_activity, + val, + Resource::Graph::DEFAULT); + } else { + ui->world()->log().warn( + fmt("Unknown value format %1% from LV2 UI\n") + % format % ui->block()->plugin()->uri().c_str()); + } +} + +static uint32_t +lv2_ui_port_index(SuilController controller, const char* port_symbol) +{ + PluginUI* const ui = (PluginUI*)controller; + + const BlockModel::Ports& ports = ui->block()->ports(); + for (uint32_t i = 0; i < ports.size(); ++i) { + if (ports[i]->symbol() == port_symbol) { + return i; + } + } + return LV2UI_INVALID_PORT_INDEX; +} + +static uint32_t +lv2_ui_subscribe(SuilController controller, + uint32_t port_index, + uint32_t protocol, + const LV2_Feature* const* features) +{ + PluginUI* const ui = (PluginUI*)controller; + SPtr<const PortModel> port = get_port(ui, port_index); + if (!port) { + return 1; + } + + ui->signal_property_changed()( + ui->block()->ports()[port_index]->uri(), + ui->world()->uris().ingen_broadcast, + ui->world()->forge().make(true), + Resource::Graph::DEFAULT); + + return 0; +} + +static uint32_t +lv2_ui_unsubscribe(SuilController controller, + uint32_t port_index, + uint32_t protocol, + const LV2_Feature* const* features) +{ + PluginUI* const ui = (PluginUI*)controller; + SPtr<const PortModel> port = get_port(ui, port_index); + if (!port) { + return 1; + } + + ui->signal_property_changed()( + ui->block()->ports()[port_index]->uri(), + ui->world()->uris().ingen_broadcast, + ui->world()->forge().make(false), + Resource::Graph::DEFAULT); + + return 0; +} + +PluginUI::PluginUI(Ingen::World* world, + SPtr<const BlockModel> block, + LilvUIs* uis, + const LilvUI* ui, + const LilvNode* ui_type) + : _world(world) + , _block(std::move(block)) + , _instance(nullptr) + , _uis(uis) + , _ui(ui) + , _ui_node(lilv_node_duplicate(lilv_ui_get_uri(ui))) + , _ui_type(lilv_node_duplicate(ui_type)) +{ +} + +PluginUI::~PluginUI() +{ + for (uint32_t i : _subscribed_ports) { + lv2_ui_unsubscribe(this, i, 0, nullptr); + } + suil_instance_free(_instance); + lilv_node_free(_ui_node); + lilv_node_free(_ui_type); + lilv_uis_free(_uis); + lilv_world_unload_resource(_world->lilv_world(), lilv_ui_get_uri(_ui)); +} + +SPtr<PluginUI> +PluginUI::create(Ingen::World* world, + SPtr<const BlockModel> block, + const LilvPlugin* plugin) +{ + if (!PluginUI::ui_host) { + PluginUI::ui_host = suil_host_new(lv2_ui_write, + lv2_ui_port_index, + lv2_ui_subscribe, + lv2_ui_unsubscribe); + } + + static const char* gtk_ui_uri = LV2_UI__GtkUI; + + LilvNode* gtk_ui = lilv_new_uri(world->lilv_world(), gtk_ui_uri); + + LilvUIs* uis = lilv_plugin_get_uis(plugin); + const LilvUI* ui = nullptr; + const LilvNode* ui_type = nullptr; + LILV_FOREACH(uis, u, uis) { + const LilvUI* this_ui = lilv_uis_get(uis, u); + if (lilv_ui_is_supported(this_ui, + suil_ui_supported, + gtk_ui, + &ui_type)) { + // TODO: Multiple UI support + ui = this_ui; + break; + } + } + + if (!ui) { + lilv_node_free(gtk_ui); + return SPtr<PluginUI>(); + } + + // Create the PluginUI, but don't instantiate yet + SPtr<PluginUI> ret(new PluginUI(world, block, uis, ui, ui_type)); + ret->_features = world->lv2_features().lv2_features( + world, const_cast<BlockModel*>(block.get())); + + return ret; +} + +bool +PluginUI::instantiate() +{ + const URIs& uris = _world->uris(); + const std::string plugin_uri = _block->plugin()->uri(); + LilvWorld* lworld = _world->lilv_world(); + + // Load seeAlso files to access data like portNotification descriptions + lilv_world_load_resource(lworld, lilv_ui_get_uri(_ui)); + + /* Subscribe (enable broadcast) for any requested port notifications. This + must be done before instantiation so responses to any events sent by the + UI's init() will be sent back to this client. */ + LilvNode* ui_portNotification = lilv_new_uri(lworld, LV2_UI__portNotification); + LilvNode* ui_plugin = lilv_new_uri(lworld, LV2_UI__plugin); + LilvNodes* notes = lilv_world_find_nodes( + lworld, lilv_ui_get_uri(_ui), ui_portNotification, nullptr); + LILV_FOREACH(nodes, n, notes) { + const LilvNode* note = lilv_nodes_get(notes, n); + const LilvNode* sym = lilv_world_get(lworld, note, uris.lv2_symbol, nullptr); + const LilvNode* plug = lilv_world_get(lworld, note, ui_plugin, nullptr); + if (!plug) { + _world->log().error(fmt("%1% UI %2% notification missing plugin\n") + % plugin_uri % lilv_node_as_string(_ui_node)); + } else if (!sym) { + _world->log().error(fmt("%1% UI %2% notification missing symbol\n") + % plugin_uri % lilv_node_as_string(_ui_node)); + } else if (!lilv_node_is_uri(plug)) { + _world->log().error(fmt("%1% UI %2% notification has non-URI plugin\n") + % plugin_uri % lilv_node_as_string(_ui_node)); + } else if (!strcmp(lilv_node_as_uri(plug), plugin_uri.c_str())) { + // Notification is valid and for this plugin + uint32_t index = lv2_ui_port_index(this, lilv_node_as_string(sym)); + if (index != LV2UI_INVALID_PORT_INDEX) { + lv2_ui_subscribe(this, index, 0, nullptr); + _subscribed_ports.insert(index); + } + } + } + lilv_nodes_free(notes); + lilv_node_free(ui_plugin); + lilv_node_free(ui_portNotification); + + const char* bundle_uri = lilv_node_as_uri(lilv_ui_get_bundle_uri(_ui)); + const char* binary_uri = lilv_node_as_uri(lilv_ui_get_binary_uri(_ui)); + char* bundle_path = lilv_file_uri_parse(bundle_uri, nullptr); + char* binary_path = lilv_file_uri_parse(binary_uri, nullptr); + + // Instantiate the actual plugin UI via Suil + _instance = suil_instance_new( + PluginUI::ui_host, + this, + LV2_UI__GtkUI, + plugin_uri.c_str(), + lilv_node_as_uri(lilv_ui_get_uri(_ui)), + lilv_node_as_uri(_ui_type), + bundle_path, + binary_path, + _features->array()); + + lilv_free(binary_path); + lilv_free(bundle_path); + + if (!_instance) { + _world->log().error("Failed to instantiate LV2 UI\n"); + // Cancel any subscriptions + for (uint32_t i : _subscribed_ports) { + lv2_ui_unsubscribe(this, i, 0, nullptr); + } + return false; + } + + return true; +} + +SuilWidget +PluginUI::get_widget() +{ + return (SuilWidget*)suil_instance_get_widget(_instance); +} + +void +PluginUI::port_event(uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void* buffer) +{ + if (_instance) { + suil_instance_port_event( + _instance, port_index, buffer_size, format, buffer); + } else { + _world->log().warn("LV2 UI port event with no instance\n"); + } +} + +bool +PluginUI::is_resizable() const +{ + LilvWorld* w = _world->lilv_world(); + const LilvNode* s = _ui_node; + LilvNode* p = lilv_new_uri(w, LV2_CORE__optionalFeature); + LilvNode* fs = lilv_new_uri(w, LV2_UI__fixedSize); + LilvNode* nrs = lilv_new_uri(w, LV2_UI__noUserResize); + + LilvNodes* fs_matches = lilv_world_find_nodes(w, s, p, fs); + LilvNodes* nrs_matches = lilv_world_find_nodes(w, s, p, nrs); + + lilv_nodes_free(nrs_matches); + lilv_nodes_free(fs_matches); + lilv_node_free(nrs); + lilv_node_free(fs); + lilv_node_free(p); + + return !fs_matches && !nrs_matches; +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/PortModel.cpp b/src/client/PortModel.cpp new file mode 100644 index 00000000..5c9a8c77 --- /dev/null +++ b/src/client/PortModel.cpp @@ -0,0 +1,78 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/PortModel.hpp" + +namespace Ingen { +namespace Client { + +void +PortModel::on_property(const URI& uri, const Atom& value) +{ + if (uri == _uris.ingen_activity) { + // Don't store activity, it is transient + signal_activity().emit(value); + return; + } + + ObjectModel::on_property(uri, value); + + if (uri == _uris.ingen_value) { + signal_value_changed().emit(value); + } +} + +bool +PortModel::supports(const URIs::Quark& value_type) const +{ + return has_property(_uris.atom_supports, value_type); +} + +bool +PortModel::port_property(const URIs::Quark& uri) const +{ + return has_property(_uris.lv2_portProperty, uri); +} + +bool +PortModel::is_uri() const +{ + // FIXME: Resource::has_property doesn't work, URI != URID + for (auto p : properties()) { + if (p.second.type() == _uris.atom_URID && + static_cast<LV2_URID>(p.second.get<int32_t>()) == _uris.atom_URID) { + return true; + } + } + return false; +} + +void +PortModel::set(SPtr<ObjectModel> model) +{ + ObjectModel::set(model); + + SPtr<PortModel> port = dynamic_ptr_cast<PortModel>(model); + if (port) { + _index = port->_index; + _direction = port->_direction; + _signal_value_changed.emit(get_property(_uris.ingen_value)); + } +} + +} // namespace Client +} // namespace Ingen diff --git a/src/client/ingen_client.cpp b/src/client/ingen_client.cpp new file mode 100644 index 00000000..fe9d6605 --- /dev/null +++ b/src/client/ingen_client.cpp @@ -0,0 +1,34 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Module.hpp" +#include "ingen/World.hpp" + +#include "ingen_config.h" + +struct IngenClientModule : public Ingen::Module { + void load(Ingen::World* world) {} +}; + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + return new IngenClientModule(); +} + +} // extern "C" diff --git a/src/client/wscript b/src/client/wscript new file mode 100644 index 00000000..df575c0d --- /dev/null +++ b/src/client/wscript @@ -0,0 +1,23 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf + +def build(bld): + obj = bld(features = 'cxx cxxshlib', + includes = ['../..'], + export_includes = ['../..'], + name = 'libingen_client', + target = 'ingen_client', + install_path = '${LIBDIR}', + use = 'libingen') + autowaf.use_lib(bld, obj, 'GLIBMM LV2 LILV SUIL RAUL SERD SORD SIGCPP') + + obj.source = ''' + BlockModel.cpp + ClientStore.cpp + GraphModel.cpp + ObjectModel.cpp + PluginModel.cpp + PluginUI.cpp + PortModel.cpp + ingen_client.cpp + ''' diff --git a/src/gui/App.cpp b/src/gui/App.cpp new file mode 100644 index 00000000..9f1a29ca --- /dev/null +++ b/src/gui/App.cpp @@ -0,0 +1,499 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <fstream> +#include <string> +#include <utility> + +#include <boost/variant/get.hpp> +#include <gtk/gtkwindow.h> +#include <gtkmm/stock.h> + +#include "ganv/Edge.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/QueuedInterface.hpp" +#include "ingen/StreamWriter.hpp" +#include "ingen/World.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/ObjectModel.hpp" +#include "ingen/client/SigClientInterface.hpp" +#include "ingen/runtime_paths.hpp" +#include "lilv/lilv.h" +#include "raul/Path.hpp" +#include "suil/suil.h" + +#include "App.hpp" +#include "ConnectWindow.hpp" +#include "GraphTreeWindow.hpp" +#include "GraphWindow.hpp" +#include "LoadPluginWindow.hpp" +#include "MessagesWindow.hpp" +#include "NodeModule.hpp" +#include "Port.hpp" +#include "RDFS.hpp" +#include "Style.hpp" +#include "SubgraphModule.hpp" +#include "ThreadedLoader.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" +#include "rgba.hpp" + +namespace Raul { class Deletable; } + +namespace Ingen { + +namespace Client { class PluginModel; } + +using namespace Client; + +namespace GUI { + +class Port; + +Gtk::Main* App::_main = nullptr; + +App::App(Ingen::World* world) + : _style(new Style(*this)) + , _about_dialog(nullptr) + , _window_factory(new WindowFactory(*this)) + , _world(world) + , _sample_rate(48000) + , _block_length(1024) + , _n_threads(1) + , _mean_run_load(0.0f) + , _min_run_load(0.0f) + , _max_run_load(0.0f) + , _enable_signal(true) + , _requested_plugins(false) + , _is_plugin(false) +{ + _world->conf().load_default("ingen", "gui.ttl"); + + WidgetFactory::get_widget_derived("connect_win", _connect_window); + WidgetFactory::get_widget_derived("messages_win", _messages_window); + WidgetFactory::get_widget_derived("graph_tree_win", _graph_tree_window); + WidgetFactory::get_widget("about_win", _about_dialog); + _connect_window->init_dialog(*this); + _messages_window->init_window(*this); + _graph_tree_window->init_window(*this); + _about_dialog->property_program_name() = "Ingen"; + _about_dialog->property_logo_icon_name() = "ingen"; + + PluginModel::set_rdf_world(*world->rdf_world()); + PluginModel::set_lilv_world(world->lilv_world()); + + using namespace std::placeholders; + world->log().set_sink(std::bind(&MessagesWindow::log, _messages_window, _1, _2, _3)); +} + +App::~App() +{ + delete _style; + delete _window_factory; +} + +SPtr<App> +App::create(Ingen::World* world) +{ + suil_init(&world->argc(), &world->argv(), SUIL_ARG_NONE); + + // Add RC file for embedded GUI Gtk style + const std::string rc_path = Ingen::data_file_path("ingen_style.rc"); + Gtk::RC::add_default_file(rc_path); + + _main = Gtk::Main::instance(); + if (!_main) { + Glib::set_application_name("Ingen"); + gtk_window_set_default_icon_name("ingen"); + _main = new Gtk::Main(&world->argc(), &world->argv()); + } + + App* app = new App(world); + + // Load configuration settings + app->style()->load_settings(); + app->style()->apply_settings(); + + // Set default window icon + app->_about_dialog->property_program_name() = "Ingen"; + app->_about_dialog->property_logo_icon_name() = "ingen"; + gtk_window_set_default_icon_name("ingen"); + + return SPtr<App>(app); +} + +void +App::run() +{ + _connect_window->start(*this, world()); + + // Run main iterations here until we're attached to the engine. Otherwise + // with 'ingen -egl' we'd get a bunch of notifications about load + // immediately before even knowing about the root graph or plugins) + while (!_connect_window->attached()) { + if (_main->iteration()) { + break; + } + } + + _main->run(); +} + +void +App::attach(SPtr<Ingen::Interface> client) +{ + assert(!_client); + assert(!_store); + assert(!_loader); + + if (_world->engine()) { + _world->engine()->register_client(client); + } + + _client = client; + _store = SPtr<ClientStore>(new ClientStore(_world->uris(), _world->log(), sig_client())); + _loader = SPtr<ThreadedLoader>(new ThreadedLoader(*this, _world->interface())); + if (!_world->store()) { + _world->set_store(_store); + } + + if (_world->conf().option("dump").get<int32_t>()) { + _dumper = SPtr<StreamWriter>(new StreamWriter(_world->uri_map(), + _world->uris(), + URI("ingen:/client"), + stderr, + ColorContext::Color::CYAN)); + + sig_client()->signal_message().connect( + sigc::mem_fun(*_dumper.get(), &StreamWriter::message)); + } + + _graph_tree_window->init(*this, *_store); + sig_client()->signal_message().connect(sigc::mem_fun(this, &App::message)); +} + +void +App::detach() +{ + if (_world->interface()) { + _window_factory->clear(); + _store->clear(); + + _loader.reset(); + _store.reset(); + _client.reset(); + _world->set_interface(SPtr<Interface>()); + } +} + +void +App::request_plugins_if_necessary() +{ + if (!_requested_plugins) { + _world->interface()->get(URI("ingen:/plugins")); + _requested_plugins = true; + } +} + +SPtr<SigClientInterface> +App::sig_client() +{ + SPtr<QueuedInterface> qi = dynamic_ptr_cast<QueuedInterface>(_client); + if (qi) { + return dynamic_ptr_cast<SigClientInterface>(qi->sink()); + } + return dynamic_ptr_cast<SigClientInterface>(_client); +} + +SPtr<Serialiser> +App::serialiser() +{ + return _world->serialiser(); +} + +void +App::message(const Message& msg) +{ + if (const Response* const r = boost::get<Response>(&msg)) { + response(r->id, r->status, r->subject); + } else if (const Error* const e = boost::get<Error>(&msg)) { + error_message(e->message); + } else if (const Put* const p = boost::get<Put>(&msg)) { + put(p->uri, p->properties, p->ctx); + } else if (const SetProperty* const s = boost::get<SetProperty>(&msg)) { + property_change(s->subject, s->predicate, s->value, s->ctx); + } +} + +void +App::response(int32_t id, Status status, const std::string& subject) +{ + if (status != Status::SUCCESS) { + std::string msg = ingen_status_string(status); + if (!subject.empty()) { + msg += ": " + subject; + } + error_message(msg); + } +} + +void +App::error_message(const std::string& str) +{ + _messages_window->post_error(str); +} + +void +App::set_property(const URI& subject, + const URI& key, + const Atom& value, + Resource::Graph ctx) +{ + // Send message to server + interface()->set_property(subject, key, value, ctx); + + /* The server does not feed back set messages (kludge to prevent control + feedback and bandwidth wastage, see Delta.cpp). So, assume everything + went as planned here and fire the signal ourselves as if the server + feedback came back immediately. */ + if (key != uris().ingen_activity) { + sig_client()->signal_message().emit(SetProperty{0, subject, key, value, ctx}); + } +} + +void +App::set_tooltip(Gtk::Widget* widget, const LilvNode* node) +{ + const std::string comment = RDFS::comment(_world, node); + if (!comment.empty()) { + widget->set_tooltip_text(comment); + } +} + +void +App::put(const URI& uri, + const Properties& properties, + Resource::Graph ctx) +{ + _enable_signal = false; + for (const auto& p : properties) { + property_change(uri, p.first, p.second); + } + _enable_signal = true; + _status_text = status_text(); + signal_status_text_changed.emit(_status_text); +} + +void +App::property_change(const URI& subject, + const URI& key, + const Atom& value, + Resource::Graph ctx) +{ + if (subject != URI("ingen:/engine")) { + return; + } else if (key == uris().param_sampleRate && value.type() == forge().Int) { + _sample_rate = value.get<int32_t>(); + } else if (key == uris().bufsz_maxBlockLength && value.type() == forge().Int) { + _block_length = value.get<int32_t>(); + } else if (key == uris().ingen_numThreads && value.type() == forge().Int) { + _n_threads = value.get<int>(); + } else if (key == uris().ingen_minRunLoad && value.type() == forge().Float) { + _min_run_load = value.get<float>(); + } else if (key == uris().ingen_meanRunLoad && value.type() == forge().Float) { + _mean_run_load = value.get<float>(); + } else if (key == uris().ingen_maxRunLoad && value.type() == forge().Float) { + _max_run_load = value.get<float>(); + } else { + _world->log().warn(fmt("Unknown engine property %1%\n") % key); + return; + } + + if (_enable_signal) { + _status_text = status_text(); + signal_status_text_changed.emit(_status_text); + } +} + +static std::string +fraction_label(float f) +{ + static const uint32_t GREEN = 0x4A8A0EFF; + static const uint32_t RED = 0x960909FF; + + const uint32_t col = rgba_interpolate(GREEN, RED, std::min(f, 1.0f)); + char col_str[8]; + snprintf(col_str, sizeof(col_str), "%02X%02X%02X", + RGBA_R(col), RGBA_G(col), RGBA_B(col)); + return (fmt("<span color='#%s'>%d%%</span>") % col_str % (f * 100)).str(); +} + +std::string +App::status_text() const +{ + return (fmt("%2.1f kHz / %.1f ms, %s, %s DSP") + % (_sample_rate / 1e3f) + % (_block_length * 1e3f / (float)_sample_rate) + % ((_n_threads == 1) + ? "1 thread" + : (fmt("%1% threads") % _n_threads).str()) + % fraction_label(_max_run_load)).str(); +} + +void +App::port_activity(Port* port) +{ + std::pair<ActivityPorts::iterator, bool> inserted = _activity_ports.emplace(port, false); + if (inserted.second) { + inserted.first->second = false; + } + + port->set_highlighted(true); +} + +void +App::activity_port_destroyed(Port* port) +{ + auto i = _activity_ports.find(port); + if (i != _activity_ports.end()) { + _activity_ports.erase(i); + } +} + +bool +App::animate() +{ + for (auto i = _activity_ports.begin(); i != _activity_ports.end(); ) { + auto next = i; + ++next; + + if ((*i).second) { // saw it last time, unhighlight and pop + (*i).first->set_highlighted(false); + _activity_ports.erase(i); + } else { + (*i).second = true; + } + + i = next; + } + + return true; +} + +/******** Event Handlers ************/ + +void +App::register_callbacks() +{ + Glib::signal_timeout().connect( + sigc::mem_fun(*this, &App::gtk_main_iteration), 33, G_PRIORITY_DEFAULT); +} + +bool +App::gtk_main_iteration() +{ + if (!_client) { + return false; + } + + animate(); + + if (_messages_window) { + _messages_window->flush(); + } + + _enable_signal = false; + if (_world->engine()) { + if (!_world->engine()->main_iteration()) { + Gtk::Main::quit(); + return false; + } + } else { + dynamic_ptr_cast<QueuedInterface>(_client)->emit(); + } + _enable_signal = true; + + return true; +} + +void +App::show_about() +{ + _about_dialog->run(); + _about_dialog->hide(); +} + +/** Prompt (if necessary) and quit application (if confirmed). + * @return true iff the application quit. + */ +bool +App::quit(Gtk::Window* dialog_parent) +{ + bool quit = true; + if (_world->engine() && _connect_window->attached()) { + Gtk::MessageDialog d( + "The engine is running in this process. Quitting will terminate Ingen." + "\n\n" "Are you sure you want to quit?", + true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_NONE, true); + if (dialog_parent) { + d.set_transient_for(*dialog_parent); + } + d.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + d.add_button(Gtk::Stock::QUIT, Gtk::RESPONSE_CLOSE); + quit = (d.run() == Gtk::RESPONSE_CLOSE); + } + + if (!quit) { + return false; + } + + Gtk::Main::quit(); + + try { + const std::string path = _world->conf().save( + _world->uri_map(), "ingen", "gui.ttl", Configuration::GUI); + std::cout << (fmt("Saved GUI settings to %1%\n") % path); + } catch (const std::exception& e) { + std::cerr << (fmt("Error saving GUI settings (%1%)\n") + % e.what()); + } + + return true; +} + +bool +App::can_control(const Client::PortModel* port) const +{ + return port->is_a(uris().lv2_ControlPort) + || port->is_a(uris().lv2_CVPort) + || (port->is_a(uris().atom_AtomPort) + && (port->supports(uris().atom_Float) + || port->supports(uris().atom_String))); +} + +uint32_t +App::sample_rate() const +{ + return _sample_rate; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/App.hpp b/src/gui/App.hpp new file mode 100644 index 00000000..75661449 --- /dev/null +++ b/src/gui/App.hpp @@ -0,0 +1,196 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_APP_HPP +#define INGEN_GUI_APP_HPP + +#include <unordered_map> +#include <string> + +#include <gtkmm/aboutdialog.h> +#include <gtkmm/main.h> +#include <gtkmm/window.h> + +#include "ingen/Atom.hpp" +#include "ingen/Message.hpp" +#include "ingen/Resource.hpp" +#include "ingen/Status.hpp" +#include "ingen/World.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "lilv/lilv.h" +#include "raul/Deletable.hpp" + +namespace Ingen { + +class Interface; +class Log; +class Port; +class Serialiser; +class StreamWriter; +class World; + +namespace Client { + +class ClientStore; +class GraphModel; +class PluginModel; +class PortModel; +class SigClientInterface; + +} + +namespace GUI { + +class ConnectWindow; +class GraphCanvas; +class GraphTreeView; +class GraphTreeWindow; +class MessagesWindow; +class Port; +class Style; +class ThreadedLoader; +class WindowFactory; + +/** Ingen Gtk Application. + * \ingroup GUI + */ +class INGEN_API App +{ +public: + ~App(); + + void error_message(const std::string& str); + + void attach(SPtr<Ingen::Interface> client); + + void detach(); + + void request_plugins_if_necessary(); + + void register_callbacks(); + bool gtk_main_iteration(); + + void show_about(); + bool quit(Gtk::Window* dialog_parent); + + void port_activity(Port* port); + void activity_port_destroyed(Port* port); + bool can_control(const Client::PortModel* port) const; + + bool signal() const { return _enable_signal; } + void enable_signals(bool b) { _enable_signal = b; } + bool disable_signals() { + bool old = _enable_signal; + _enable_signal = false; + return old; + } + + void set_property(const URI& subject, + const URI& key, + const Atom& value, + Resource::Graph ctx = Resource::Graph::DEFAULT); + + /** Set the tooltip for a widget from its RDF documentation. */ + void set_tooltip(Gtk::Widget* widget, const LilvNode* node); + + uint32_t sample_rate() const; + + bool is_plugin() const { return _is_plugin; } + void set_is_plugin(bool b) { _is_plugin = b; } + + ConnectWindow* connect_window() const { return _connect_window; } + MessagesWindow* messages_dialog() const { return _messages_window; } + GraphTreeWindow* graph_tree() const { return _graph_tree_window; } + Style* style() const { return _style; } + WindowFactory* window_factory() const { return _window_factory; } + + Ingen::Forge& forge() const { return _world->forge(); } + SPtr<Ingen::Interface> interface() const { return _world->interface(); } + SPtr<Ingen::Interface> client() const { return _client; } + SPtr<Client::ClientStore> store() const { return _store; } + SPtr<ThreadedLoader> loader() const { return _loader; } + + SPtr<Client::SigClientInterface> sig_client(); + + SPtr<Serialiser> serialiser(); + + static SPtr<App> create(Ingen::World* world); + + void run(); + + std::string status_text() const; + + sigc::signal<void, const std::string&> signal_status_text_changed; + + inline Ingen::World* world() const { return _world; } + inline Ingen::URIs& uris() const { return _world->uris(); } + inline Ingen::Log& log() const { return _world->log(); } + +protected: + explicit App(Ingen::World* world); + + void message(const Ingen::Message& msg); + + bool animate(); + void response(int32_t id, Ingen::Status status, const std::string& subject); + + void put(const URI& uri, + const Properties& properties, + Resource::Graph ctx); + + void property_change(const URI& subject, + const URI& key, + const Atom& value, + Resource::Graph ctx = Resource::Graph::DEFAULT); + + static Gtk::Main* _main; + + SPtr<Ingen::Interface> _client; + SPtr<Client::ClientStore> _store; + SPtr<ThreadedLoader> _loader; + SPtr<StreamWriter> _dumper; + + Style* _style; + + ConnectWindow* _connect_window; + MessagesWindow* _messages_window; + GraphTreeWindow* _graph_tree_window; + Gtk::AboutDialog* _about_dialog; + WindowFactory* _window_factory; + + Ingen::World* _world; + + int32_t _sample_rate; + int32_t _block_length; + int32_t _n_threads; + float _mean_run_load; + float _min_run_load; + float _max_run_load; + std::string _status_text; + + typedef std::unordered_map<Port*, bool> ActivityPorts; + ActivityPorts _activity_ports; + + bool _enable_signal; + bool _requested_plugins; + bool _is_plugin; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_APP_HPP diff --git a/src/gui/Arc.cpp b/src/gui/Arc.cpp new file mode 100644 index 00000000..c14b2e88 --- /dev/null +++ b/src/gui/Arc.cpp @@ -0,0 +1,44 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "Arc.hpp" +#include "ingen/client/ArcModel.hpp" +#include "ingen/client/BlockModel.hpp" + +#define NS_INTERNALS "http://drobilla.net/ns/ingen-internals#" + +namespace Ingen { +namespace GUI { + +Arc::Arc(Ganv::Canvas& canvas, + SPtr<const Client::ArcModel> model, + Ganv::Node* src, + Ganv::Node* dst) + : Ganv::Edge(canvas, src, dst) + , _arc_model(model) +{ + SPtr<const Client::ObjectModel> tparent = model->tail()->parent(); + SPtr<const Client::BlockModel> tparent_block; + if ((tparent_block = dynamic_ptr_cast<const Client::BlockModel>(tparent))) { + if (tparent_block->plugin_uri() == NS_INTERNALS "BlockDelay") { + g_object_set(_gobj, "dash-length", 4.0, NULL); + set_constraining(false); + } + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/Arc.hpp b/src/gui/Arc.hpp new file mode 100644 index 00000000..382ca305 --- /dev/null +++ b/src/gui/Arc.hpp @@ -0,0 +1,52 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_ARC_HPP +#define INGEN_GUI_ARC_HPP + +#include <cassert> + +#include "ganv/Edge.hpp" +#include "ingen/types.hpp" + +namespace Ingen { + +namespace Client { class ArcModel; } + +namespace GUI { + +/** An Arc (directed edge) in a Graph. + * + * \ingroup GUI + */ +class Arc : public Ganv::Edge +{ +public: + Arc(Ganv::Canvas& canvas, + SPtr<const Client::ArcModel> model, + Ganv::Node* src, + Ganv::Node* dst); + + SPtr<const Client::ArcModel> model() const { return _arc_model; } + +private: + SPtr<const Client::ArcModel> _arc_model; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_ARC_HPP diff --git a/src/gui/BreadCrumbs.cpp b/src/gui/BreadCrumbs.cpp new file mode 100644 index 00000000..ae7882e3 --- /dev/null +++ b/src/gui/BreadCrumbs.cpp @@ -0,0 +1,229 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <list> +#include <string> + +#include <boost/variant/get.hpp> + +#include "ingen/client/SigClientInterface.hpp" + +#include "App.hpp" +#include "BreadCrumbs.hpp" + +namespace Ingen { +namespace GUI { + +using std::string; + +BreadCrumbs::BreadCrumbs(App& app) + : Gtk::HBox() + , _active_path("/") + , _full_path("/") + , _enable_signal(true) +{ + app.sig_client()->signal_message().connect( + sigc::mem_fun(this, &BreadCrumbs::message)); + + set_can_focus(false); +} + +SPtr<GraphView> +BreadCrumbs::view(const Raul::Path& path) +{ + for (const auto& b : _breadcrumbs) { + if (b->path() == path) { + return b->view(); + } + } + + return SPtr<GraphView>(); +} + +/** Sets up the crumbs to display `path`. + * + * If `path` is already part of the shown path, it will be selected and the + * children preserved. + */ +void +BreadCrumbs::build(Raul::Path path, SPtr<GraphView> view) +{ + bool old_enable_signal = _enable_signal; + _enable_signal = false; + + if (!_breadcrumbs.empty() && (path.is_parent_of(_full_path) || path == _full_path)) { + // Moving to a path we already contain, just switch the active button + for (const auto& b : _breadcrumbs) { + if (b->path() == path) { + b->set_active(true); + if (!b->view()) { + b->set_view(view); + } + + // views are expensive, having two around for the same graph is a bug + assert(b->view() == view); + + } else { + b->set_active(false); + } + } + + _active_path = path; + _enable_signal = old_enable_signal; + + } else if (!_breadcrumbs.empty() && path.is_child_of(_full_path)) { + // Moving to a child of the full path, just append crumbs (preserve view cache) + + string suffix = path.substr(_full_path.length()); + while (suffix.length() > 0) { + if (suffix[0] == '/') { + suffix = suffix.substr(1); + } + const string name = suffix.substr(0, suffix.find("/")); + _full_path = _full_path.child(Raul::Symbol(name)); + BreadCrumb* but = create_crumb(_full_path, view); + pack_start(*but, false, false, 1); + _breadcrumbs.push_back(but); + but->show(); + if (suffix.find("/") == string::npos) { + break; + } else { + suffix = suffix.substr(suffix.find("/")+1); + } + } + + for (const auto& b : _breadcrumbs) { + b->set_active(false); + } + _breadcrumbs.back()->set_active(true); + + } else { + // Rebuild from scratch + // Getting here is bad unless absolutely necessary, since the GraphView cache is lost + + _full_path = path; + _active_path = path; + + // Empty existing breadcrumbs + for (const auto& b : _breadcrumbs) { + remove(*b); + } + _breadcrumbs.clear(); + + // Add root + BreadCrumb* root_but = create_crumb(Raul::Path("/"), view); + pack_start(*root_but, false, false, 1); + _breadcrumbs.push_front(root_but); + root_but->set_active(root_but->path() == _active_path); + + Raul::Path working_path("/"); + string suffix = path.substr(1); + while (suffix.length() > 0) { + if (suffix[0] == '/') { + suffix = suffix.substr(1); + } + const string name = suffix.substr(0, suffix.find("/")); + working_path = working_path.child(Raul::Symbol(name)); + BreadCrumb* but = create_crumb(working_path, view); + pack_start(*but, false, false, 1); + _breadcrumbs.push_back(but); + but->set_active(working_path == _active_path); + but->show(); + if (suffix.find("/") == string::npos) { + break; + } else { + suffix = suffix.substr(suffix.find("/")+1); + } + } + } + + _enable_signal = old_enable_signal; +} + +/** Create a new crumb, assigning it a reference to `view` if their paths + * match, otherwise ignoring `view`. + */ +BreadCrumbs::BreadCrumb* +BreadCrumbs::create_crumb(const Raul::Path& path, + SPtr<GraphView> view) +{ + BreadCrumb* but = manage( + new BreadCrumb(path, + ((view && path == view->graph()->path()) + ? view : SPtr<GraphView>()))); + + but->signal_toggled().connect( + sigc::bind(sigc::mem_fun(this, &BreadCrumbs::breadcrumb_clicked), + but)); + + return but; +} + +void +BreadCrumbs::breadcrumb_clicked(BreadCrumb* crumb) +{ + if (_enable_signal) { + _enable_signal = false; + + if (!crumb->get_active()) { + // Tried to turn off the current active button, bad user, no cookie + crumb->set_active(true); + } else { + signal_graph_selected.emit(crumb->path(), crumb->view()); + if (crumb->path() != _active_path) { + crumb->set_active(false); + } + } + _enable_signal = true; + } +} + +void +BreadCrumbs::message(const Message& msg) +{ + if (const Del* const del = boost::get<Del>(&msg)) { + object_destroyed(del->uri); + } +} + +void +BreadCrumbs::object_destroyed(const URI& uri) +{ + for (auto i = _breadcrumbs.begin(); i != _breadcrumbs.end(); ++i) { + if ((*i)->path() == uri.c_str()) { + // Remove all crumbs after the removed one (inclusive) + for (auto j = i; j != _breadcrumbs.end(); ) { + BreadCrumb* bc = *j; + j = _breadcrumbs.erase(j); + remove(*bc); + } + break; + } + } +} + +void +BreadCrumbs::object_moved(const Raul::Path& old_path, const Raul::Path& new_path) +{ + for (const auto& b : _breadcrumbs) { + if (b->path() == old_path) { + b->set_path(new_path); + } + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/BreadCrumbs.hpp b/src/gui/BreadCrumbs.hpp new file mode 100644 index 00000000..467d3bfc --- /dev/null +++ b/src/gui/BreadCrumbs.hpp @@ -0,0 +1,119 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_BREADCRUMBS_HPP +#define INGEN_GUI_BREADCRUMBS_HPP + +#include <list> + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/togglebutton.h> + +#include "raul/Path.hpp" + +#include "ingen/client/GraphModel.hpp" +#include "ingen/types.hpp" + +#include "GraphView.hpp" + +namespace Ingen { +namespace GUI { + +/** Collection of breadcrumb buttons forming a path. + * This doubles as a cache for GraphViews. + * + * \ingroup GUI + */ +class BreadCrumbs : public Gtk::HBox +{ +public: + explicit BreadCrumbs(App& app); + + SPtr<GraphView> view(const Raul::Path& path); + + void build(Raul::Path path, SPtr<GraphView> view); + + sigc::signal<void, const Raul::Path&, SPtr<GraphView> > signal_graph_selected; + +private: + /** Breadcrumb button. + * + * Each Breadcrumb stores a reference to a GraphView for quick switching. + * So, the amount of allocated GraphViews at a given time is equal to the + * number of visible breadcrumbs (which is the perfect cache for GUI + * responsiveness balanced with mem consumption). + * + * \ingroup GUI + */ + class BreadCrumb : public Gtk::ToggleButton + { + public: + BreadCrumb(const Raul::Path& path, SPtr<GraphView> view = SPtr<GraphView>()) + : _path(path) + , _view(view) + { + assert(!view || view->graph()->path() == path); + set_border_width(0); + set_path(path); + set_can_focus(false); + show_all(); + } + + void set_view(SPtr<GraphView> view) { + assert(!view || view->graph()->path() == _path); + _view = view; + } + + const Raul::Path& path() const { return _path; } + SPtr<GraphView> view() const { return _view; } + + void set_path(const Raul::Path& path) { + remove(); + const char* text = (path.is_root()) ? "/" : path.symbol(); + Gtk::Label* lab = manage(new Gtk::Label(text)); + lab->set_padding(0, 0); + lab->show(); + add(*lab); + + if (_view && _view->graph()->path() != path) + _view.reset(); + } + + private: + Raul::Path _path; + SPtr<GraphView> _view; + }; + + BreadCrumb* create_crumb(const Raul::Path& path, + SPtr<GraphView> view = SPtr<GraphView>()); + + void breadcrumb_clicked(BreadCrumb* crumb); + + void message(const Message& msg); + void object_destroyed(const URI& uri); + void object_moved(const Raul::Path& old_path, const Raul::Path& new_path); + + Raul::Path _active_path; + Raul::Path _full_path; + bool _enable_signal; + std::list<BreadCrumb*> _breadcrumbs; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_BREADCRUMBS_HPP diff --git a/src/gui/ConnectWindow.cpp b/src/gui/ConnectWindow.cpp new file mode 100644 index 00000000..458a43dd --- /dev/null +++ b/src/gui/ConnectWindow.cpp @@ -0,0 +1,572 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <limits> +#include <sstream> +#include <string> + +#include <boost/variant/get.hpp> +#include <gtkmm/stock.h> + +#include "raul/Process.hpp" + +#include "ingen/Configuration.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/QueuedInterface.hpp" +#include "ingen/World.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/SigClientInterface.hpp" +#include "ingen/client/SocketClient.hpp" +#include "ingen_config.h" + +#include "App.hpp" +#include "ConnectWindow.hpp" +#include "WindowFactory.hpp" + +using namespace Ingen::Client; + +namespace Ingen { +namespace GUI { + +ConnectWindow::ConnectWindow(BaseObjectType* cobject, + Glib::RefPtr<Gtk::Builder> xml) + : Dialog(cobject) + , _xml(std::move(xml)) + , _icon(nullptr) + , _progress_bar(nullptr) + , _progress_label(nullptr) + , _url_entry(nullptr) + , _server_radio(nullptr) + , _port_spinbutton(nullptr) + , _launch_radio(nullptr) + , _internal_radio(nullptr) + , _activate_button(nullptr) + , _deactivate_button(nullptr) + , _disconnect_button(nullptr) + , _connect_button(nullptr) + , _quit_button(nullptr) + , _mode(Mode::CONNECT_REMOTE) + , _connect_uri("unix:///tmp/ingen.sock") + , _ping_id(-1) + , _attached(false) + , _finished_connecting(false) + , _widgets_loaded(false) + , _connect_stage(0) + , _quit_flag(false) +{ +} + +void +ConnectWindow::message(const Message& msg) +{ + if (const Response* const r = boost::get<Response>(&msg)) { + ingen_response(r->id, r->status, r->subject); + } else if (const Error* const e = boost::get<Error>(&msg)) { + error(e->message); + } +} + +void +ConnectWindow::error(const std::string& msg) +{ + if (!is_visible()) { + present(); + set_connecting_widget_states(); + } + + if (_progress_label) { + _progress_label->set_text(msg); + } + + if (_app && _app->world()) { + _app->world()->log().error(msg + "\n"); + } +} + +void +ConnectWindow::start(App& app, Ingen::World* world) +{ + _app = &app; + + if (world->engine()) { + _mode = Mode::INTERNAL; + } + + set_connected_to(world->interface()); + connect(bool(world->interface())); +} + +void +ConnectWindow::ingen_response(int32_t id, + Status status, + const std::string& subject) +{ + if (id == _ping_id) { + if (status != Status::SUCCESS) { + error("Failed to get root patch"); + } else { + _attached = true; + } + } +} + +void +ConnectWindow::set_connected_to(SPtr<Ingen::Interface> engine) +{ + _app->world()->set_interface(engine); + + if (!_widgets_loaded) { + return; + } + + if (engine) { + _icon->set(Gtk::Stock::CONNECT, Gtk::ICON_SIZE_LARGE_TOOLBAR); + _progress_bar->set_fraction(1.0); + _progress_label->set_text("Connected to engine"); + _url_entry->set_sensitive(false); + _url_entry->set_text(engine->uri().string()); + _connect_button->set_sensitive(false); + _disconnect_button->set_label("gtk-disconnect"); + _disconnect_button->set_sensitive(true); + _port_spinbutton->set_sensitive(false); + _launch_radio->set_sensitive(false); + _internal_radio->set_sensitive(false); + _activate_button->set_sensitive(true); + _deactivate_button->set_sensitive(true); + + } else { + _icon->set(Gtk::Stock::DISCONNECT, Gtk::ICON_SIZE_LARGE_TOOLBAR); + _progress_bar->set_fraction(0.0); + _connect_button->set_sensitive(true); + _disconnect_button->set_sensitive(false); + _internal_radio->set_sensitive(true); + _server_radio->set_sensitive(true); + _launch_radio->set_sensitive(true); + _activate_button->set_sensitive(false); + _deactivate_button->set_sensitive(false); + + if (_mode == Mode::CONNECT_REMOTE) { + _url_entry->set_sensitive(true); + } else if (_mode == Mode::LAUNCH_REMOTE) { + _port_spinbutton->set_sensitive(true); + } + + _progress_label->set_text(std::string("Disconnected")); + } +} + +void +ConnectWindow::set_connecting_widget_states() +{ + if (!_widgets_loaded) { + return; + } + + _connect_button->set_sensitive(false); + _disconnect_button->set_label("gtk-cancel"); + _disconnect_button->set_sensitive(true); + _server_radio->set_sensitive(false); + _launch_radio->set_sensitive(false); + _internal_radio->set_sensitive(false); + _url_entry->set_sensitive(false); + _port_spinbutton->set_sensitive(false); +} + +bool +ConnectWindow::connect_remote(const URI& uri) +{ + Ingen::World* world = _app->world(); + + SPtr<SigClientInterface> sci(new SigClientInterface()); + SPtr<QueuedInterface> qi(new QueuedInterface(sci)); + + SPtr<Ingen::Interface> iface(world->new_interface(uri, qi)); + if (iface) { + world->set_interface(iface); + _app->attach(qi); + _app->register_callbacks(); + return true; + } + + return false; +} + +/** Set up initial connect stage and launch connect callback. */ +void +ConnectWindow::connect(bool existing) +{ + if (_app->client()) { + error("Already connected"); + return; + } else if (_attached) { + _attached = false; + } + + set_connecting_widget_states(); + _connect_stage = 0; + + Ingen::World* world = _app->world(); + + if (_mode == Mode::CONNECT_REMOTE) { + std::string uri_str = world->conf().option("connect").ptr<char>(); + if (existing) { + uri_str = world->interface()->uri(); + _connect_stage = 1; + SPtr<Client::SocketClient> client = dynamic_ptr_cast<Client::SocketClient>(world->interface()); + if (client) { + _app->attach(client->respondee()); + _app->register_callbacks(); + } else { + error("Connected with invalid client interface type"); + return; + } + } else if (_widgets_loaded) { + uri_str = _url_entry->get_text(); + } + + if (!URI::is_valid(uri_str)) { + error((fmt("Invalid socket URI %1%") % uri_str).str()); + return; + } + + _connect_uri = URI(uri_str); + + } else if (_mode == Mode::LAUNCH_REMOTE) { + const std::string port = std::to_string(_port_spinbutton->get_value_as_int()); + const char* cmd[] = { "ingen", "-e", "-E", port.c_str(), nullptr }; + + if (!Raul::Process::launch(cmd)) { + error("Failed to launch engine process"); + return; + } + + _connect_uri = URI(std::string("tcp://localhost:") + port); + + } else if (_mode == Mode::INTERNAL) { + if (!world->engine()) { + if (!world->load_module("server")) { + error("Failed to load server module"); + return; + } else if (!world->load_module("jack")) { + error("Failed to load jack module"); + return; + } else if (!world->engine()->activate()) { + error("Failed to activate engine"); + return; + } + } + } + + set_connecting_widget_states(); + if (_widgets_loaded) { + _progress_label->set_text("Connecting..."); + } + Glib::signal_timeout().connect( + sigc::mem_fun(this, &ConnectWindow::gtk_callback), 33); +} + +void +ConnectWindow::disconnect() +{ + _connect_stage = -1; + _attached = false; + + _app->detach(); + set_connected_to(SPtr<Ingen::Interface>()); + + if (!_widgets_loaded) { + return; + } + + _activate_button->set_sensitive(false); + _deactivate_button->set_sensitive(false); + + _progress_bar->set_fraction(0.0); + _connect_button->set_sensitive(true); + _disconnect_button->set_sensitive(false); +} + +void +ConnectWindow::activate() +{ + if (!_app->interface()) { + return; + } + + _app->interface()->set_property(URI("ingen:/driver"), + _app->uris().ingen_enabled, + _app->forge().make(true)); +} + +void +ConnectWindow::deactivate() +{ + if (!_app->interface()) { + return; + } + + _app->interface()->set_property(URI("ingen:/driver"), + _app->uris().ingen_enabled, + _app->forge().make(false)); +} + +void +ConnectWindow::on_show() +{ + if (!_widgets_loaded) { + load_widgets(); + } + + if (_attached) { + set_connected_to(_app->interface()); + } + + Gtk::Dialog::on_show(); +} + +void +ConnectWindow::load_widgets() +{ + _xml->get_widget("connect_icon", _icon); + _xml->get_widget("connect_progress_bar", _progress_bar); + _xml->get_widget("connect_progress_label", _progress_label); + _xml->get_widget("connect_server_radiobutton", _server_radio); + _xml->get_widget("connect_url_entry", _url_entry); + _xml->get_widget("connect_launch_radiobutton", _launch_radio); + _xml->get_widget("connect_port_spinbutton", _port_spinbutton); + _xml->get_widget("connect_internal_radiobutton", _internal_radio); + _xml->get_widget("connect_activate_button", _activate_button); + _xml->get_widget("connect_deactivate_button", _deactivate_button); + _xml->get_widget("connect_disconnect_button", _disconnect_button); + _xml->get_widget("connect_connect_button", _connect_button); + _xml->get_widget("connect_quit_button", _quit_button); + + _server_radio->signal_toggled().connect( + sigc::mem_fun(this, &ConnectWindow::server_toggled)); + _launch_radio->signal_toggled().connect( + sigc::mem_fun(this, &ConnectWindow::launch_toggled)); + _internal_radio->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::internal_toggled)); + _activate_button->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::activate)); + _deactivate_button->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::deactivate)); + _disconnect_button->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::disconnect)); + _connect_button->signal_clicked().connect( + sigc::bind(sigc::mem_fun(this, &ConnectWindow::connect), false)); + _quit_button->signal_clicked().connect( + sigc::mem_fun(this, &ConnectWindow::quit_clicked)); + + _url_entry->set_text(_app->world()->conf().option("connect").ptr<char>()); + if (URI::is_valid(_url_entry->get_text())) { + _connect_uri = URI(_url_entry->get_text()); + } + + _port_spinbutton->set_range(1, std::numeric_limits<uint16_t>::max()); + _port_spinbutton->set_increments(1, 100); + _port_spinbutton->set_value( + _app->world()->conf().option("engine-port").get<int32_t>()); + + _progress_bar->set_pulse_step(0.01); + _widgets_loaded = true; + + server_toggled(); +} + +void +ConnectWindow::on_hide() +{ + Gtk::Dialog::on_hide(); + if (_app->window_factory()->num_open_graph_windows() == 0) { + quit(); + } +} + +void +ConnectWindow::quit_clicked() +{ + if (_app->quit(this)) { + _quit_flag = true; + } +} + +void +ConnectWindow::server_toggled() +{ + _url_entry->set_sensitive(true); + _port_spinbutton->set_sensitive(false); + _mode = Mode::CONNECT_REMOTE; +} + +void +ConnectWindow::launch_toggled() +{ + _url_entry->set_sensitive(false); + _port_spinbutton->set_sensitive(true); + _mode = Mode::LAUNCH_REMOTE; +} + +void +ConnectWindow::internal_toggled() +{ + _url_entry->set_sensitive(false); + _port_spinbutton->set_sensitive(false); + _mode = Mode::INTERNAL; +} + +void +ConnectWindow::next_stage() +{ + static const char* labels[] = { + "Connecting...", + "Pinging engine...", + "Attaching to engine...", + "Requesting root graph...", + "Loading plugins...", + "Connected" + }; + + + ++_connect_stage; + if (_widgets_loaded) { + _progress_label->set_text(labels[_connect_stage]); + } +} + +bool +ConnectWindow::gtk_callback() +{ + /* If I call this a "state machine" it's not ugly code any more */ + + if (_quit_flag) { + return false; // deregister this callback + } + + // Timing stuff for repeated attach attempts + timeval now; + gettimeofday(&now, nullptr); + static const timeval start = now; + static timeval last = now; + static unsigned attempts = 0; + + // Show if attempted connection goes on for a noticeable amount of time + if (!is_visible()) { + const float ms_since_start = (now.tv_sec - start.tv_sec) * 1000.0f + + (now.tv_usec - start.tv_usec) * 0.001f; + if (ms_since_start > 500) { + present(); + set_connecting_widget_states(); + } + } + + if (_connect_stage == 0) { + const float ms_since_last = (now.tv_sec - last.tv_sec) * 1000.0f + + (now.tv_usec - last.tv_usec) * 0.001f; + if (ms_since_last >= 250) { + last = now; + if (_mode == Mode::INTERNAL) { + SPtr<SigClientInterface> client(new SigClientInterface()); + _app->world()->interface()->set_respondee(client); + _app->attach(client); + _app->register_callbacks(); + next_stage(); + } else if (connect_remote(_connect_uri)) { + next_stage(); + } + } + } else if (_connect_stage == 1) { + _attached = false; + _app->sig_client()->signal_message().connect( + sigc::mem_fun(this, &ConnectWindow::message)); + + _ping_id = g_random_int_range(1, std::numeric_limits<int32_t>::max()); + _app->interface()->set_response_id(_ping_id); + _app->interface()->get(URI("ingen:/engine")); + last = now; + attempts = 0; + next_stage(); + + } else if (_connect_stage == 2) { + if (_attached) { + next_stage(); + } else { + const float ms_since_last = (now.tv_sec - last.tv_sec) * 1000.0f + + (now.tv_usec - last.tv_usec) * 0.001f; + if (attempts > 10) { + error("Failed to ping engine"); + _connect_stage = -1; + } else if (ms_since_last > 1000) { + _app->interface()->set_response_id(_ping_id); + _app->interface()->get(URI("ingen:/engine")); + last = now; + ++attempts; + } + } + } else if (_connect_stage == 3) { + _app->interface()->get(URI(main_uri().string() + "/")); + next_stage(); + } else if (_connect_stage == 4) { + if (_app->store()->size() > 0) { + SPtr<const GraphModel> root = dynamic_ptr_cast<const GraphModel>( + _app->store()->object(Raul::Path("/"))); + if (root) { + set_connected_to(_app->interface()); + _app->window_factory()->present_graph(root); + next_stage(); + } + } + } else if (_connect_stage == 5) { + hide(); + _connect_stage = 0; // set ourselves up for next time (if there is one) + _finished_connecting = true; + _app->interface()->set_response_id(1); + return false; // deregister this callback + } + + if (_widgets_loaded) { + _progress_bar->pulse(); + } + + if (_connect_stage == -1) { // we were cancelled + if (_widgets_loaded) { + _icon->set(Gtk::Stock::DISCONNECT, Gtk::ICON_SIZE_LARGE_TOOLBAR); + _progress_bar->set_fraction(0.0); + _connect_button->set_sensitive(true); + _disconnect_button->set_sensitive(false); + _disconnect_button->set_label("gtk-disconnect"); + _progress_label->set_text(std::string("Disconnected")); + } + return false; + } else { + return true; + } +} + +void +ConnectWindow::quit() +{ + _quit_flag = true; + Gtk::Main::quit(); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/ConnectWindow.hpp b/src/gui/ConnectWindow.hpp new file mode 100644 index 00000000..08560361 --- /dev/null +++ b/src/gui/ConnectWindow.hpp @@ -0,0 +1,116 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_CONNECTWINDOW_HPP +#define INGEN_GUI_CONNECTWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/image.h> +#include <gtkmm/label.h> +#include <gtkmm/progressbar.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/spinbutton.h> + +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +#include "Window.hpp" + +namespace Ingen { +namespace GUI { + +class App; + +/** The initially visible "Connect to engine" window. + * + * This handles actually connecting to the engine and making sure everything + * is ready before really launching the app. + * + * \ingroup GUI + */ +class ConnectWindow : public Dialog +{ +public: + ConnectWindow(BaseObjectType* cobject, + Glib::RefPtr<Gtk::Builder> xml); + + void set_connected_to(SPtr<Ingen::Interface> engine); + void start(App& app, Ingen::World* world); + + bool attached() const { return _finished_connecting; } + bool quit_flag() const { return _quit_flag; } + +private: + enum class Mode { CONNECT_REMOTE, LAUNCH_REMOTE, INTERNAL }; + + void message(const Message& msg); + + void error(const std::string& msg); + + void ingen_response(int32_t id, Status status, const std::string& subject); + + void server_toggled(); + void launch_toggled(); + void internal_toggled(); + + void disconnect(); + void next_stage(); + bool connect_remote(const URI& uri); + void connect(bool existing); + void activate(); + void deactivate(); + void quit_clicked(); + void on_show(); + void on_hide(); + + void load_widgets(); + void set_connecting_widget_states(); + + bool gtk_callback(); + void quit(); + + const Glib::RefPtr<Gtk::Builder> _xml; + + Gtk::Image* _icon; + Gtk::ProgressBar* _progress_bar; + Gtk::Label* _progress_label; + Gtk::Entry* _url_entry; + Gtk::RadioButton* _server_radio; + Gtk::SpinButton* _port_spinbutton; + Gtk::RadioButton* _launch_radio; + Gtk::RadioButton* _internal_radio; + Gtk::Button* _activate_button; + Gtk::Button* _deactivate_button; + Gtk::Button* _disconnect_button; + Gtk::Button* _connect_button; + Gtk::Button* _quit_button; + + Mode _mode; + URI _connect_uri; + int32_t _ping_id; + bool _attached; + bool _finished_connecting; + bool _widgets_loaded; + int _connect_stage; + bool _quit_flag; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_CONNECTWINDOW_HPP diff --git a/src/gui/GraphBox.cpp b/src/gui/GraphBox.cpp new file mode 100644 index 00000000..6f9969be --- /dev/null +++ b/src/gui/GraphBox.cpp @@ -0,0 +1,922 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <sstream> +#include <string> + +#include <boost/format.hpp> +#include <glib/gstdio.h> +#include <glibmm/fileutils.h> +#include <gtkmm/stock.h> + +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "BreadCrumbs.hpp" +#include "ConnectWindow.hpp" +#include "GraphCanvas.hpp" +#include "GraphTreeWindow.hpp" +#include "GraphView.hpp" +#include "GraphWindow.hpp" +#include "LoadGraphWindow.hpp" +#include "LoadPluginWindow.hpp" +#include "MessagesWindow.hpp" +#include "NewSubgraphWindow.hpp" +#include "Style.hpp" +#include "ThreadedLoader.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" +#include "ingen_config.h" + +#ifdef HAVE_WEBKIT +#include <webkit/webkit.h> +#endif + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +static const int STATUS_CONTEXT_ENGINE = 0; +static const int STATUS_CONTEXT_GRAPH = 1; +static const int STATUS_CONTEXT_HOVER = 2; + +GraphBox::GraphBox(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::VBox(cobject) + , _app(nullptr) + , _window(nullptr) + , _breadcrumbs(nullptr) + , _has_shown_documentation(false) + , _enable_signal(true) +{ + property_visible() = false; + + xml->get_widget("graph_win_alignment", _alignment); + xml->get_widget("graph_win_status_bar", _status_bar); + xml->get_widget("graph_import_menuitem", _menu_import); + xml->get_widget("graph_save_menuitem", _menu_save); + xml->get_widget("graph_save_as_menuitem", _menu_save_as); + xml->get_widget("graph_export_image_menuitem", _menu_export_image); + xml->get_widget("graph_redo_menuitem", _menu_redo); + xml->get_widget("graph_undo_menuitem", _menu_undo); + xml->get_widget("graph_cut_menuitem", _menu_cut); + xml->get_widget("graph_copy_menuitem", _menu_copy); + xml->get_widget("graph_paste_menuitem", _menu_paste); + xml->get_widget("graph_delete_menuitem", _menu_delete); + xml->get_widget("graph_select_all_menuitem", _menu_select_all); + xml->get_widget("graph_close_menuitem", _menu_close); + xml->get_widget("graph_quit_menuitem", _menu_quit); + xml->get_widget("graph_view_control_window_menuitem", _menu_view_control_window); + xml->get_widget("graph_view_engine_window_menuitem", _menu_view_engine_window); + xml->get_widget("graph_properties_menuitem", _menu_view_graph_properties); + xml->get_widget("graph_parent_menuitem", _menu_parent); + xml->get_widget("graph_refresh_menuitem", _menu_refresh); + xml->get_widget("graph_fullscreen_menuitem", _menu_fullscreen); + xml->get_widget("graph_animate_signals_menuitem", _menu_animate_signals); + xml->get_widget("graph_sprung_layout_menuitem", _menu_sprung_layout); + xml->get_widget("graph_human_names_menuitem", _menu_human_names); + xml->get_widget("graph_show_port_names_menuitem", _menu_show_port_names); + xml->get_widget("graph_zoom_in_menuitem", _menu_zoom_in); + xml->get_widget("graph_zoom_out_menuitem", _menu_zoom_out); + xml->get_widget("graph_zoom_normal_menuitem", _menu_zoom_normal); + xml->get_widget("graph_zoom_full_menuitem", _menu_zoom_full); + xml->get_widget("graph_increase_font_size_menuitem", _menu_increase_font_size); + xml->get_widget("graph_decrease_font_size_menuitem", _menu_decrease_font_size); + xml->get_widget("graph_normal_font_size_menuitem", _menu_normal_font_size); + xml->get_widget("graph_doc_pane_menuitem", _menu_show_doc_pane); + xml->get_widget("graph_status_bar_menuitem", _menu_show_status_bar); + xml->get_widget("graph_arrange_menuitem", _menu_arrange); + xml->get_widget("graph_view_messages_window_menuitem", _menu_view_messages_window); + xml->get_widget("graph_view_graph_tree_window_menuitem", _menu_view_graph_tree_window); + xml->get_widget("graph_help_about_menuitem", _menu_help_about); + xml->get_widget("graph_documentation_paned", _doc_paned); + xml->get_widget("graph_documentation_scrolledwindow", _doc_scrolledwindow); + + _menu_view_control_window->property_sensitive() = false; + _menu_import->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_import)); + _menu_save->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_save)); + _menu_save_as->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_save_as)); + _menu_export_image->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_export_image)); + _menu_redo->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_redo)); + _menu_undo->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_undo)); + _menu_copy->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_copy)); + _menu_paste->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_paste)); + _menu_delete->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_delete)); + _menu_select_all->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_select_all)); + _menu_close->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_close)); + _menu_quit->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_quit)); + _menu_parent->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_parent_activated)); + _menu_refresh->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_refresh_activated)); + _menu_fullscreen->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_fullscreen_toggled)); + _menu_animate_signals->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_animate_signals_toggled)); + _menu_sprung_layout->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_sprung_layout_toggled)); + _menu_human_names->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_human_names_toggled)); + _menu_show_doc_pane->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_doc_pane_toggled)); + _menu_show_status_bar->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_status_bar_toggled)); + _menu_show_port_names->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_port_names_toggled)); + _menu_arrange->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_arrange)); + _menu_zoom_in->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_zoom_in)); + _menu_zoom_out->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_zoom_out)); + _menu_zoom_normal->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_zoom_normal)); + _menu_zoom_full->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_zoom_full)); + _menu_increase_font_size->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_increase_font_size)); + _menu_decrease_font_size->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_decrease_font_size)); + _menu_normal_font_size->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_normal_font_size)); + _menu_view_engine_window->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_show_engine)); + _menu_view_graph_properties->signal_activate().connect( + sigc::mem_fun(this, &GraphBox::event_show_properties)); + + Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get(); + clipboard->signal_owner_change().connect( + sigc::mem_fun(this, &GraphBox::event_clipboard_changed)); + +#ifdef __APPLE__ + _menu_paste->set_sensitive(true); +#endif + + _status_label = Gtk::manage(new Gtk::Label("STATUS")); + _status_bar->pack_start(*_status_label, false, true, 0); + _status_label->show(); +} + +GraphBox::~GraphBox() +{ + delete _breadcrumbs; +} + +SPtr<GraphBox> +GraphBox::create(App& app, SPtr<const GraphModel> graph) +{ + GraphBox* result = nullptr; + Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create("graph_win"); + xml->get_widget_derived("graph_win_vbox", result); + result->init_box(app); + result->set_graph(graph, SPtr<GraphView>()); + + if (app.is_plugin()) { + result->_menu_close->set_sensitive(false); + result->_menu_quit->set_sensitive(false); + } + + return SPtr<GraphBox>(result); +} + +void +GraphBox::init_box(App& app) +{ + _app = &app; + + const URI engine_uri(_app->interface()->uri()); + if (engine_uri == "ingen:/clients/event_writer") { + _status_bar->push("Running internal engine", STATUS_CONTEXT_ENGINE); + } else { + _status_bar->push( + (fmt("Connected to %1%") % engine_uri).str(), + STATUS_CONTEXT_ENGINE); + } + + _menu_view_messages_window->signal_activate().connect( + sigc::mem_fun<void>(_app->messages_dialog(), &MessagesWindow::present)); + _menu_view_graph_tree_window->signal_activate().connect( + sigc::mem_fun<void>(_app->graph_tree(), &GraphTreeWindow::present)); + + _menu_help_about->signal_activate().connect( + sigc::hide_return(sigc::mem_fun(_app, &App::show_about))); + + _breadcrumbs = new BreadCrumbs(*_app); + _breadcrumbs->signal_graph_selected.connect( + sigc::mem_fun(this, &GraphBox::set_graph_from_path)); + + _status_label->set_markup(app.status_text()); + app.signal_status_text_changed.connect( + sigc::mem_fun(*this, &GraphBox::set_status_text)); +} + +void +GraphBox::set_status_text(const std::string& text) +{ + _status_label->set_markup(text); +} + +void +GraphBox::set_graph_from_path(const Raul::Path& path, SPtr<GraphView> view) +{ + if (view) { + assert(view->graph()->path() == path); + _app->window_factory()->present_graph(view->graph(), _window, view); + } else { + SPtr<const GraphModel> model = dynamic_ptr_cast<const GraphModel>( + _app->store()->object(path)); + if (model) { + _app->window_factory()->present_graph(model, _window); + } + } +} + +/** Sets the graph for this box and initializes everything. + * + * If `view` is NULL, a new view will be created. + */ +void +GraphBox::set_graph(SPtr<const GraphModel> graph, + SPtr<GraphView> view) +{ + if (!graph || graph == _graph) { + return; + } + + _enable_signal = false; + + new_port_connection.disconnect(); + removed_port_connection.disconnect(); + edit_mode_connection.disconnect(); + _entered_connection.disconnect(); + _left_connection.disconnect(); + + _status_bar->pop(STATUS_CONTEXT_GRAPH); + + _graph = graph; + _view = view; + + if (!_view) { + _view = _breadcrumbs->view(graph->path()); + } + + if (!_view) { + _view = GraphView::create(*_app, graph); + } + + assert(_view); + + graph->signal_property().connect( + sigc::mem_fun(this, &GraphBox::property_changed)); + + if (!_view->canvas()->supports_sprung_layout()) { + _menu_sprung_layout->set_active(false); + _menu_sprung_layout->set_sensitive(false); + } + + // Add view to our alignment + if (_view->get_parent()) { + _view->get_parent()->remove(*_view.get()); + } + + _alignment->remove(); + _alignment->add(*_view.get()); + + if (_breadcrumbs->get_parent()) { + _breadcrumbs->get_parent()->remove(*_breadcrumbs); + } + + _view->breadcrumb_container()->remove(); + _view->breadcrumb_container()->add(*_breadcrumbs); + _view->breadcrumb_container()->show(); + + _breadcrumbs->build(graph->path(), _view); + _breadcrumbs->show(); + + _menu_view_control_window->property_sensitive() = false; + + for (const auto& p : graph->ports()) { + if (_app->can_control(p.get())) { + _menu_view_control_window->property_sensitive() = true; + break; + } + } + + _menu_parent->property_sensitive() = bool(graph->parent()); + + new_port_connection = graph->signal_new_port().connect( + sigc::mem_fun(this, &GraphBox::graph_port_added)); + removed_port_connection = graph->signal_removed_port().connect( + sigc::mem_fun(this, &GraphBox::graph_port_removed)); + + show(); + _alignment->show_all(); + + _menu_human_names->set_active( + _app->world()->conf().option("human-names").get<int32_t>()); + _menu_show_port_names->set_active( + _app->world()->conf().option("port-labels").get<int32_t>()); + + _doc_paned->set_position(std::numeric_limits<int>::max()); + _doc_scrolledwindow->hide(); + + _enable_signal = true; +} + +void +GraphBox::graph_port_added(SPtr<const PortModel> port) +{ + if (port->is_input() && _app->can_control(port.get())) { + _menu_view_control_window->property_sensitive() = true; + } +} + +void +GraphBox::graph_port_removed(SPtr<const PortModel> port) +{ + if (!(port->is_input() && _app->can_control(port.get()))) { + return; + } + + for (const auto& p : _graph->ports()) { + if (p->is_input() && _app->can_control(p.get())) { + _menu_view_control_window->property_sensitive() = true; + return; + } + } + + _menu_view_control_window->property_sensitive() = false; +} + +void +GraphBox::property_changed(const URI& predicate, const Atom& value) +{ + if (predicate == _app->uris().ingen_sprungLayout) { + if (value.type() == _app->uris().forge.Bool) { + _menu_sprung_layout->set_active(value.get<int32_t>()); + } + } +} + +void +GraphBox::set_documentation(const std::string& doc, bool html) +{ + _doc_scrolledwindow->remove(); + if (doc.empty()) { + _doc_scrolledwindow->hide(); + return; + } +#ifdef HAVE_WEBKIT + WebKitWebView* view = WEBKIT_WEB_VIEW(webkit_web_view_new()); + webkit_web_view_load_html_string(view, doc.c_str(), ""); + Gtk::Widget* widget = Gtk::manage(Glib::wrap(GTK_WIDGET(view))); + _doc_scrolledwindow->add(*widget); + widget->show(); +#else + Gtk::TextView* view = Gtk::manage(new Gtk::TextView()); + view->get_buffer()->set_text(doc); + view->set_wrap_mode(Gtk::WRAP_WORD); + _doc_scrolledwindow->add(*view); + view->show(); +#endif +} + +void +GraphBox::show_status(const ObjectModel* model) +{ + std::stringstream msg; + msg << model->path(); + + const PortModel* port = nullptr; + const BlockModel* block = nullptr; + + if ((port = dynamic_cast<const PortModel*>(model))) { + show_port_status(port, port->value()); + + } else if ((block = dynamic_cast<const BlockModel*>(model))) { + const PluginModel* plugin = dynamic_cast<const PluginModel*>(block->plugin()); + if (plugin) { + msg << ((boost::format(" (%1%)") % plugin->human_name()).str()); + } + _status_bar->push(msg.str(), STATUS_CONTEXT_HOVER); + } +} + +void +GraphBox::show_port_status(const PortModel* port, const Atom& value) +{ + std::stringstream msg; + msg << port->path(); + + const BlockModel* parent = dynamic_cast<const BlockModel*>(port->parent().get()); + if (parent) { + const PluginModel* plugin = dynamic_cast<const PluginModel*>(parent->plugin()); + if (plugin) { + const std::string& human_name = plugin->port_human_name(port->index()); + if (!human_name.empty()) { + msg << " (" << human_name << ")"; + } + } + } + + if (value.is_valid()) { + msg << " = " << _app->forge().str(value, true); + } + + _status_bar->pop(STATUS_CONTEXT_HOVER); + _status_bar->push(msg.str(), STATUS_CONTEXT_HOVER); +} + +void +GraphBox::object_entered(const ObjectModel* model) +{ + show_status(model); +} + +void +GraphBox::object_left(const ObjectModel* model) +{ + _status_bar->pop(STATUS_CONTEXT_GRAPH); + _status_bar->pop(STATUS_CONTEXT_HOVER); +} + +void +GraphBox::event_show_engine() +{ + if (_graph) { + _app->connect_window()->show(); + } +} + +void +GraphBox::event_clipboard_changed(GdkEventOwnerChange* ev) +{ + Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get(); + _menu_paste->set_sensitive(clipboard->wait_is_text_available()); +} + +void +GraphBox::event_show_properties() +{ + _app->window_factory()->present_properties(_graph); +} + +void +GraphBox::event_import() +{ + _app->window_factory()->present_load_graph(_graph); +} + +void +GraphBox::event_save() +{ + const Atom& document = _graph->get_property(_app->uris().ingen_file); + if (!document.is_valid() || document.type() != _app->uris().forge.URI) { + event_save_as(); + } else { + save_graph(URI(document.ptr<char>())); + } +} + +void +GraphBox::error(const Glib::ustring& message, + const Glib::ustring& secondary_text) +{ + Gtk::MessageDialog dialog( + message, true, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dialog.set_secondary_text(secondary_text); + if (_window) { + dialog.set_transient_for(*_window); + } + dialog.run(); +} + +bool +GraphBox::confirm(const Glib::ustring& message, + const Glib::ustring& secondary_text) +{ + Gtk::MessageDialog dialog( + message, true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true); + dialog.set_secondary_text(secondary_text); + if (_window) { + dialog.set_transient_for(*_window); + } + return dialog.run() == Gtk::RESPONSE_YES; +} + +void +GraphBox::save_graph(const URI& uri) +{ + if (_app->interface()->uri().string().substr(0, 3) == "tcp") { + _status_bar->push( + (boost::format("Saved %1% to %2% on client") + % _graph->path() % uri).str(), + STATUS_CONTEXT_GRAPH); + _app->loader()->save_graph(_graph, uri); + } else { + _status_bar->push( + (boost::format("Saved %1% to %2% on server") + % _graph->path() % uri).str(), + STATUS_CONTEXT_GRAPH); + _app->interface()->copy(_graph->uri(), uri); + } +} + +void +GraphBox::event_save_as() +{ + const URIs& uris = _app->uris(); + while (true) { + Gtk::FileChooserDialog dialog( + "Save Graph", Gtk::FILE_CHOOSER_ACTION_SAVE); + if (_window) { + dialog.set_transient_for(*_window); + } + + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + Gtk::Button* save_button = dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + save_button->property_has_default() = true; + + Gtk::FileFilter filt; + filt.add_pattern("*.ingen"); + filt.set_name("Ingen bundles"); + dialog.set_filter(filt); + + // Set current folder to most sensible default + const Atom& document = _graph->get_property(uris.ingen_file); + const Atom& dir = _app->world()->conf().option("graph-directory"); + if (document.type() == uris.forge.URI) { + dialog.set_uri(document.ptr<char>()); + } else if (dir.is_valid()) { + dialog.set_current_folder(dir.ptr<char>()); + } + + if (dialog.run() != Gtk::RESPONSE_OK) { + break; + } + + std::string filename = dialog.get_filename(); + std::string basename = Glib::path_get_basename(filename); + + if (basename.find('.') == std::string::npos) { + filename += ".ingen"; + basename += ".ingen"; + } else if (filename.substr(filename.length() - 4) == ".ttl") { + const Glib::ustring dir = Glib::path_get_dirname(filename); + if (dir.substr(dir.length() - 6) != ".ingen") { + error("<b>File does not appear to be in an Ingen bundle."); + } + } else if (filename.substr(filename.length() - 6) != ".ingen") { + error("<b>Ingen bundles must end in \".ingen\"</b>"); + continue; + } + + const std::string symbol(basename.substr(0, basename.find('.'))); + + if (!Raul::Symbol::is_valid(symbol)) { + error( + "<b>Ingen bundle names must be valid symbols.</b>", + "All characters must be _, a-z, A-Z, or 0-9, but the first may not be 0-9."); + continue; + } + + //_graph->set_property(uris.lv2_symbol, Atom(symbol.c_str())); + + bool confirmed = true; + if (Glib::file_test(filename, Glib::FILE_TEST_IS_DIR)) { + if (Glib::file_test(Glib::build_filename(filename, "manifest.ttl"), + Glib::FILE_TEST_EXISTS)) { + confirmed = confirm( + (boost::format("<b>The bundle \"%1%\" already exists." + " Replace it?</b>") % basename).str()); + } else { + confirmed = confirm( + (boost::format("<b>A directory named \"%1%\" already exists," + "but is not an Ingen bundle. " + "Save into it anyway?</b>") % basename).str(), + "This will create at least 2 .ttl files in this directory," + "and possibly several more files and/or directories, recursively. " + "Existing files will be overwritten."); + } + } else if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) { + confirmed = confirm( + (boost::format("<b>A file named \"%1%\" already exists. " + "Replace it with an Ingen bundle?</b>") + % basename).str()); + if (confirmed) { + ::g_remove(filename.c_str()); + } + } + + if (confirmed) { + const Glib::ustring uri = Glib::filename_to_uri(filename); + save_graph(URI(uri)); + + const_cast<GraphModel*>(_graph.get())->set_property( + uris.ingen_file, + _app->forge().alloc_uri(uri.c_str())); + } + + _app->world()->conf().set( + "graph-directory", + _app->world()->forge().alloc(dialog.get_current_folder())); + + break; + } +} + +void +GraphBox::event_export_image() +{ + Gtk::FileChooserDialog dialog("Export Image", Gtk::FILE_CHOOSER_ACTION_SAVE); + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + dialog.set_default_response(Gtk::RESPONSE_OK); + if (_window) { + dialog.set_transient_for(*_window); + } + + typedef std::map<std::string, std::string> Types; + Types types; + types["*.dot"] = "Graphviz DOT"; + types["*.pdf"] = "Portable Document Format"; + types["*.ps"] = "PostScript"; + types["*.svg"] = "Scalable Vector Graphics"; + for (Types::const_iterator t = types.begin(); t != types.end(); ++t) { + Gtk::FileFilter filt; + filt.add_pattern(t->first); + filt.set_name(t->second); + dialog.add_filter(filt); + if (t->first == "*.pdf") { + dialog.set_filter(filt); + } + } + + Gtk::CheckButton* bg_but = new Gtk::CheckButton("Draw _Background", true); + Gtk::Alignment* extra = new Gtk::Alignment(1.0, 0.5, 0.0, 0.0); + bg_but->set_active(true); + extra->add(*Gtk::manage(bg_but)); + extra->show_all(); + dialog.set_extra_widget(*Gtk::manage(extra)); + + if (dialog.run() == Gtk::RESPONSE_OK) { + const std::string filename = dialog.get_filename(); + if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) { + Gtk::MessageDialog confirm( + std::string("File exists! Overwrite ") + filename + "?", + true, Gtk::MESSAGE_WARNING, Gtk::BUTTONS_YES_NO, true); + confirm.set_transient_for(dialog); + if (confirm.run() != Gtk::RESPONSE_YES) { + return; + } + } + _view->canvas()->export_image(filename.c_str(), bg_but->get_active()); + _status_bar->push((boost::format("Rendered %1% to %2%") + % _graph->path() % filename).str(), + STATUS_CONTEXT_GRAPH); + } +} + +void +GraphBox::event_copy() +{ + if (_view) { + _view->canvas()->copy_selection(); + } +} + +void +GraphBox::event_redo() +{ + _app->interface()->redo(); +} + +void +GraphBox::event_undo() +{ + _app->interface()->undo(); +} + +void +GraphBox::event_paste() +{ + if (_view) { + _view->canvas()->paste(); + } +} + +void +GraphBox::event_delete() +{ + if (_view) { + _view->canvas()->destroy_selection(); + } +} + +void +GraphBox::event_select_all() +{ + if (_view) { + _view->canvas()->select_all(); + } +} + +void +GraphBox::event_close() +{ + if (_window) { + _app->window_factory()->remove_graph_window(_window); + } +} + +void +GraphBox::event_quit() +{ + _app->quit(_window); +} + +void +GraphBox::event_zoom_in() +{ + _view->canvas()->set_font_size(_view->canvas()->get_font_size() + 1.0); +} + +void +GraphBox::event_zoom_out() +{ + _view->canvas()->set_font_size(_view->canvas()->get_font_size() - 1.0); +} + +void +GraphBox::event_zoom_normal() +{ + _view->canvas()->set_zoom(1.0); +} + +void +GraphBox::event_zoom_full() +{ + _view->canvas()->zoom_full(); +} + +void +GraphBox::event_increase_font_size() +{ + _view->canvas()->set_font_size(_view->canvas()->get_font_size() + 1.0); +} +void +GraphBox::event_decrease_font_size() +{ + _view->canvas()->set_font_size(_view->canvas()->get_font_size() - 1.0); +} +void +GraphBox::event_normal_font_size() +{ + _view->canvas()->set_font_size(_view->canvas()->get_default_font_size()); +} + +void +GraphBox::event_arrange() +{ + _app->interface()->bundle_begin(); + _view->canvas()->arrange(); + _app->interface()->bundle_end(); +} + +void +GraphBox::event_parent_activated() +{ + SPtr<Client::GraphModel> parent = dynamic_ptr_cast<Client::GraphModel>(_graph->parent()); + if (parent) { + _app->window_factory()->present_graph(parent, _window); + } +} + +void +GraphBox::event_refresh_activated() +{ + _app->interface()->get(_graph->uri()); +} + +void +GraphBox::event_fullscreen_toggled() +{ + // FIXME: ugh, use GTK signals to track state and know for sure + static bool is_fullscreen = false; + + if (_window) { + if (!is_fullscreen) { + _window->fullscreen(); + is_fullscreen = true; + } else { + _window->unfullscreen(); + is_fullscreen = false; + } + } +} + +void +GraphBox::event_doc_pane_toggled() +{ + if (_menu_show_doc_pane->get_active()) { + _doc_scrolledwindow->show_all(); + if (!_has_shown_documentation) { + const Gtk::Allocation allocation = get_allocation(); + _doc_paned->set_position(allocation.get_width() / 1.61803399); + _has_shown_documentation = true; + } + } else { + _doc_scrolledwindow->hide(); + } +} + +void +GraphBox::event_status_bar_toggled() +{ + if (_menu_show_status_bar->get_active()) { + _status_bar->show(); + } else { + _status_bar->hide(); + } +} + +void +GraphBox::event_animate_signals_toggled() +{ + _app->interface()->set_property( + URI("ingen:/clients/this"), + _app->uris().ingen_broadcast, + _app->forge().make((bool)_menu_animate_signals->get_active())); +} + +void +GraphBox::event_sprung_layout_toggled() +{ + const bool sprung = _menu_sprung_layout->get_active(); + + _view->canvas()->set_sprung_layout(sprung); + + Properties properties; + properties.emplace(_app->uris().ingen_sprungLayout, + _app->forge().make(sprung)); + _app->interface()->put(_graph->uri(), properties); +} + +void +GraphBox::event_human_names_toggled() +{ + _view->canvas()->show_human_names(_menu_human_names->get_active()); + _app->world()->conf().set( + "human-names", + _app->world()->forge().make(_menu_human_names->get_active())); +} + +void +GraphBox::event_port_names_toggled() +{ + _app->world()->conf().set( + "port-labels", + _app->world()->forge().make(_menu_show_port_names->get_active())); + if (_menu_show_port_names->get_active()) { + _view->canvas()->set_direction(GANV_DIRECTION_RIGHT); + _view->canvas()->show_port_names(true); + } else { + _view->canvas()->set_direction(GANV_DIRECTION_DOWN); + _view->canvas()->show_port_names(false); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphBox.hpp b/src/gui/GraphBox.hpp new file mode 100644 index 00000000..fd9bf9c0 --- /dev/null +++ b/src/gui/GraphBox.hpp @@ -0,0 +1,213 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPH_BOX_HPP +#define INGEN_GUI_GRAPH_BOX_HPP + +#include <string> + +#include <gtkmm/alignment.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/menushell.h> +#include <gtkmm/messagedialog.h> +#include <gtkmm/paned.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/statusbar.h> + +#include "ingen/ingen.h" +#include "ingen/types.hpp" + +#include "Window.hpp" + +namespace Raul { +class Atom; +class Path; +} + +namespace Ingen { + +class URI; + +namespace Client { +class GraphModel; +class PortModel; +class ObjectModel; +} + +namespace GUI { + +class BreadCrumbs; +class LoadGraphBox; +class LoadPluginWindow; +class NewSubgraphWindow; +class GraphDescriptionWindow; +class GraphView; +class GraphWindow; +class SubgraphModule; + +/** A window for a graph. + * + * \ingroup GUI + */ +class INGEN_API GraphBox : public Gtk::VBox +{ +public: + GraphBox(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + ~GraphBox(); + + static SPtr<GraphBox> create( + App& app, SPtr<const Client::GraphModel> graph); + + void init_box(App& app); + + void set_status_text(const std::string& text); + + void set_graph(SPtr<const Client::GraphModel> graph, + SPtr<GraphView> view); + + void set_window(GraphWindow* win) { _window = win; } + + bool documentation_is_visible() { return _doc_scrolledwindow->is_visible(); } + void set_documentation(const std::string& doc, bool html); + + SPtr<const Client::GraphModel> graph() const { return _graph; } + SPtr<GraphView> view() const { return _view; } + + void show_port_status(const Client::PortModel* port, + const Atom& value); + + void set_graph_from_path(const Raul::Path& path, SPtr<GraphView> view); + + void object_entered(const Client::ObjectModel* model); + void object_left(const Client::ObjectModel* model); + +private: + void graph_port_added(SPtr<const Client::PortModel> port); + void graph_port_removed(SPtr<const Client::PortModel> port); + void property_changed(const URI& predicate, const Atom& value); + void show_status(const Client::ObjectModel* model); + + void error(const Glib::ustring& message, + const Glib::ustring& secondary_text=""); + + bool confirm(const Glib::ustring& message, + const Glib::ustring& secondary_text=""); + + void save_graph(const URI& uri); + + void event_import(); + void event_save(); + void event_save_as(); + void event_export_image(); + void event_redo(); + void event_undo(); + void event_copy(); + void event_paste(); + void event_delete(); + void event_select_all(); + void event_close(); + void event_quit(); + void event_parent_activated(); + void event_refresh_activated(); + void event_fullscreen_toggled(); + void event_doc_pane_toggled(); + void event_status_bar_toggled(); + void event_animate_signals_toggled(); + void event_sprung_layout_toggled(); + void event_human_names_toggled(); + void event_port_names_toggled(); + void event_zoom_in(); + void event_zoom_out(); + void event_zoom_normal(); + void event_zoom_full(); + void event_increase_font_size(); + void event_decrease_font_size(); + void event_normal_font_size(); + void event_arrange(); + void event_show_properties(); + void event_show_engine(); + void event_clipboard_changed(GdkEventOwnerChange* ev); + + App* _app; + SPtr<const Client::GraphModel> _graph; + SPtr<GraphView> _view; + GraphWindow* _window; + + sigc::connection new_port_connection; + sigc::connection removed_port_connection; + sigc::connection edit_mode_connection; + + Gtk::MenuItem* _menu_import; + Gtk::MenuItem* _menu_save; + Gtk::MenuItem* _menu_save_as; + Gtk::MenuItem* _menu_export_image; + Gtk::MenuItem* _menu_redo; + Gtk::MenuItem* _menu_undo; + Gtk::MenuItem* _menu_cut; + Gtk::MenuItem* _menu_copy; + Gtk::MenuItem* _menu_paste; + Gtk::MenuItem* _menu_delete; + Gtk::MenuItem* _menu_select_all; + Gtk::MenuItem* _menu_close; + Gtk::MenuItem* _menu_quit; + Gtk::CheckMenuItem* _menu_animate_signals; + Gtk::CheckMenuItem* _menu_sprung_layout; + Gtk::CheckMenuItem* _menu_human_names; + Gtk::CheckMenuItem* _menu_show_port_names; + Gtk::CheckMenuItem* _menu_show_doc_pane; + Gtk::CheckMenuItem* _menu_show_status_bar; + Gtk::MenuItem* _menu_zoom_in; + Gtk::MenuItem* _menu_zoom_out; + Gtk::MenuItem* _menu_zoom_normal; + Gtk::MenuItem* _menu_zoom_full; + Gtk::MenuItem* _menu_increase_font_size; + Gtk::MenuItem* _menu_decrease_font_size; + Gtk::MenuItem* _menu_normal_font_size; + Gtk::MenuItem* _menu_parent; + Gtk::MenuItem* _menu_refresh; + Gtk::MenuItem* _menu_fullscreen; + Gtk::MenuItem* _menu_arrange; + Gtk::MenuItem* _menu_view_engine_window; + Gtk::MenuItem* _menu_view_control_window; + Gtk::MenuItem* _menu_view_graph_properties; + Gtk::MenuItem* _menu_view_messages_window; + Gtk::MenuItem* _menu_view_graph_tree_window; + Gtk::MenuItem* _menu_help_about; + + Gtk::Alignment* _alignment; + BreadCrumbs* _breadcrumbs; + Gtk::Statusbar* _status_bar; + Gtk::Label* _status_label; + + Gtk::HPaned* _doc_paned; + Gtk::ScrolledWindow* _doc_scrolledwindow; + + sigc::connection _entered_connection; + sigc::connection _left_connection; + + /** Invisible bin used to store breadcrumbs when not shown by a view */ + Gtk::Alignment _breadcrumb_bin; + + bool _has_shown_documentation; + bool _enable_signal; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPH_BOX_HPP diff --git a/src/gui/GraphCanvas.cpp b/src/gui/GraphCanvas.cpp new file mode 100644 index 00000000..a17915a5 --- /dev/null +++ b/src/gui/GraphCanvas.cpp @@ -0,0 +1,898 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <algorithm> +#include <cassert> +#include <map> +#include <set> +#include <string> + +#include <boost/optional.hpp> +#include <gtkmm/stock.h> + +#include "ganv/Canvas.hpp" +#include "ganv/Circle.hpp" +#include "ingen/ClashAvoider.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/World.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" + +#include "App.hpp" +#include "Arc.hpp" +#include "GraphCanvas.hpp" +#include "GraphPortModule.hpp" +#include "GraphWindow.hpp" +#include "LoadPluginWindow.hpp" +#include "NewSubgraphWindow.hpp" +#include "NodeModule.hpp" +#include "PluginMenu.hpp" +#include "Port.hpp" +#include "SubgraphModule.hpp" +#include "ThreadedLoader.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +using std::string; + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +static int +port_order(const GanvPort* a, const GanvPort* b, void* data) +{ + const Port* pa = dynamic_cast<const Port*>(Glib::wrap(a)); + const Port* pb = dynamic_cast<const Port*>(Glib::wrap(b)); + if (pa && pb) { + return ((int)pa->model()->index() - (int)pb->model()->index()); + } + return 0; +} + +GraphCanvas::GraphCanvas(App& app, + SPtr<const GraphModel> graph, + int width, + int height) + : Canvas(width, height) + , _app(app) + , _graph(std::move(graph)) + , _auto_position_count(0) + , _menu_x(0) + , _menu_y(0) + , _paste_count(0) + , _menu(nullptr) + , _internal_menu(nullptr) + , _plugin_menu(nullptr) + , _human_names(true) + , _show_port_names(true) + , _menu_dirty(false) +{ + Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create("canvas_menu"); + xml->get_widget("canvas_menu", _menu); + + xml->get_widget("canvas_menu_add_audio_input", _menu_add_audio_input); + xml->get_widget("canvas_menu_add_audio_output", _menu_add_audio_output); + xml->get_widget("canvas_menu_add_cv_input", _menu_add_cv_input); + xml->get_widget("canvas_menu_add_cv_output", _menu_add_cv_output); + xml->get_widget("canvas_menu_add_control_input", _menu_add_control_input); + xml->get_widget("canvas_menu_add_control_output", _menu_add_control_output); + xml->get_widget("canvas_menu_add_event_input", _menu_add_event_input); + xml->get_widget("canvas_menu_add_event_output", _menu_add_event_output); + xml->get_widget("canvas_menu_load_plugin", _menu_load_plugin); + xml->get_widget("canvas_menu_load_graph", _menu_load_graph); + xml->get_widget("canvas_menu_new_graph", _menu_new_graph); + xml->get_widget("canvas_menu_edit", _menu_edit); + xml->get_widget("canvas_menu_properties", _menu_properties); + + const URIs& uris = _app.uris(); + + // Add port menu items + _menu_add_audio_input->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "audio_in", "Audio In", uris.lv2_AudioPort, false)); + _menu_add_audio_output->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "audio_out", "Audio Out", uris.lv2_AudioPort, true)); + _menu_add_cv_input->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "cv_in", "CV In", uris.lv2_CVPort, false)); + _menu_add_cv_output->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "cv_out", "CV Out", uris.lv2_CVPort, true)); + _menu_add_control_input->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "control_in", "Control In", uris.lv2_ControlPort, false)); + _menu_add_control_output->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "control_out", "Control Out", uris.lv2_ControlPort, true)); + _menu_add_event_input->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "event_in", "Event In", uris.atom_AtomPort, false)); + _menu_add_event_output->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &GraphCanvas::menu_add_port), + "event_out", "Event Out", uris.atom_AtomPort, true)); + + signal_event.connect( + sigc::mem_fun(this, &GraphCanvas::on_event)); + signal_connect.connect( + sigc::mem_fun(this, &GraphCanvas::connect)); + signal_disconnect.connect( + sigc::mem_fun(this, &GraphCanvas::disconnect)); + + // Connect to model signals to track state + _graph->signal_new_block().connect( + sigc::mem_fun(this, &GraphCanvas::add_block)); + _graph->signal_removed_block().connect( + sigc::mem_fun(this, &GraphCanvas::remove_block)); + _graph->signal_new_port().connect( + sigc::mem_fun(this, &GraphCanvas::add_port)); + _graph->signal_removed_port().connect( + sigc::mem_fun(this, &GraphCanvas::remove_port)); + _graph->signal_new_arc().connect( + sigc::mem_fun(this, &GraphCanvas::connection)); + _graph->signal_removed_arc().connect( + sigc::mem_fun(this, &GraphCanvas::disconnection)); + + _app.store()->signal_new_plugin().connect( + sigc::mem_fun(this, &GraphCanvas::add_plugin)); + _app.store()->signal_plugin_deleted().connect( + sigc::mem_fun(this, &GraphCanvas::remove_plugin)); + + // Connect widget signals to do things + _menu_load_plugin->signal_activate().connect( + sigc::mem_fun(this, &GraphCanvas::menu_load_plugin)); + _menu_load_graph->signal_activate().connect( + sigc::mem_fun(this, &GraphCanvas::menu_load_graph)); + _menu_new_graph->signal_activate().connect( + sigc::mem_fun(this, &GraphCanvas::menu_new_graph)); + _menu_properties->signal_activate().connect( + sigc::mem_fun(this, &GraphCanvas::menu_properties)); + + show_human_names(app.world()->conf().option("human-names").get<int32_t>()); + show_port_names(app.world()->conf().option("port-labels").get<int32_t>()); + set_port_order(port_order, nullptr); +} + +void +GraphCanvas::show_menu(bool position, unsigned button, uint32_t time) +{ + _app.request_plugins_if_necessary(); + + if (!_internal_menu || _menu_dirty) { + build_menus(); + } + + if (position) { + _menu->popup(sigc::mem_fun(this, &GraphCanvas::auto_menu_position), button, time); + } else { + _menu->popup(button, time); + } +} + +void +GraphCanvas::build_menus() +{ + // Build (or clear existing) internal plugin menu + if (_internal_menu) { + _internal_menu->items().clear(); + } else { + _menu->items().push_back( + Gtk::Menu_Helpers::ImageMenuElem( + "In_ternal", + *(manage(new Gtk::Image(Gtk::Stock::EXECUTE, Gtk::ICON_SIZE_MENU))))); + Gtk::MenuItem* internal_menu_item = &(_menu->items().back()); + _internal_menu = Gtk::manage(new Gtk::Menu()); + internal_menu_item->set_submenu(*_internal_menu); + _menu->reorder_child(*internal_menu_item, 4); + } + + // Build skeleton LV2 plugin class heirarchy for 'Plugin' menu + if (_plugin_menu) { + _plugin_menu->clear(); + } else { + _plugin_menu = Gtk::manage(new PluginMenu(*_app.world())); + _menu->items().push_back( + Gtk::Menu_Helpers::ImageMenuElem( + "_Plugin", + *(manage(new Gtk::Image(Gtk::Stock::EXECUTE, Gtk::ICON_SIZE_MENU))))); + Gtk::MenuItem* plugin_menu_item = &(_menu->items().back()); + plugin_menu_item->set_submenu(*_plugin_menu); + _menu->reorder_child(*plugin_menu_item, 5); + _plugin_menu->signal_load_plugin.connect( + sigc::mem_fun(this, &GraphCanvas::load_plugin)); + } + + // Add known plugins to menu heirarchy + SPtr<const ClientStore::Plugins> plugins = _app.store()->plugins(); + for (const auto& p : *plugins.get()) { + add_plugin(p.second); + } + + _menu_dirty = false; +} + +void +GraphCanvas::build() +{ + const Store::const_range kids = _app.store()->children_range(_graph); + + // Create modules for blocks + for (Store::const_iterator i = kids.first; i != kids.second; ++i) { + SPtr<BlockModel> block = dynamic_ptr_cast<BlockModel>(i->second); + if (block && block->parent() == _graph) { + add_block(block); + } + } + + // Create pseudo modules for ports (ports on this canvas, not on our module) + for (const auto& p : _graph->ports()) { + add_port(p); + } + + // Create arcs + for (const auto& a : _graph->arcs()) { + connection(dynamic_ptr_cast<ArcModel>(a.second)); + } +} + +static void +show_module_human_names(GanvNode* node, void* data) +{ + bool b = *(bool*)data; + if (GANV_IS_MODULE(node)) { + Ganv::Module* module = Glib::wrap(GANV_MODULE(node)); + NodeModule* nmod = dynamic_cast<NodeModule*>(module); + if (nmod) { + nmod->show_human_names(b); + } + + GraphPortModule* pmod = dynamic_cast<GraphPortModule*>(module); + if (pmod) { + pmod->show_human_names(b); + } + } +} + +void +GraphCanvas::show_human_names(bool b) +{ + _human_names = b; + _app.world()->conf().set("human-names", _app.forge().make(b)); + + for_each_node(show_module_human_names, &b); +} + +static void +ensure_port_labels(GanvNode* node, void* data) +{ + if (GANV_IS_MODULE(node)) { + Ganv::Module* module = Glib::wrap(GANV_MODULE(node)); + for (Ganv::Port* p : *module) { + Ingen::GUI::Port* port = dynamic_cast<Ingen::GUI::Port*>(p); + if (port) { + port->ensure_label(); + } + } + } +} + +void +GraphCanvas::show_port_names(bool b) +{ + ganv_canvas_set_direction(gobj(), b ? GANV_DIRECTION_RIGHT : GANV_DIRECTION_DOWN); + for_each_node(ensure_port_labels, &b); +} + +void +GraphCanvas::add_plugin(SPtr<PluginModel> p) +{ + if (_internal_menu && _app.uris().ingen_Internal == p->type()) { + _internal_menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + std::string("_") + p->human_name(), + sigc::bind(sigc::mem_fun(this, &GraphCanvas::load_plugin), p))); + } else if (_plugin_menu) { + _plugin_menu->add_plugin(p); + } +} + +void +GraphCanvas::remove_plugin(const URI& uri) +{ + // Flag menus as dirty so they will be rebuilt when needed next + _menu_dirty = true; +} + +void +GraphCanvas::add_block(SPtr<const BlockModel> bm) +{ + SPtr<const GraphModel> pm = dynamic_ptr_cast<const GraphModel>(bm); + NodeModule* module; + if (pm) { + module = SubgraphModule::create(*this, pm, _human_names); + } else { + module = NodeModule::create(*this, bm, _human_names); + } + + module->show(); + _views.emplace(bm, module); + if (_pastees.find(bm->path()) != _pastees.end()) { + module->set_selected(true); + } +} + +void +GraphCanvas::remove_block(SPtr<const BlockModel> bm) +{ + auto i = _views.find(bm); + + if (i != _views.end()) { + const guint n_ports = i->second->num_ports(); + for (gint p = n_ports - 1; p >= 0; --p) { + delete i->second->get_port(p); + } + delete i->second; + _views.erase(i); + } +} + +void +GraphCanvas::add_port(SPtr<const PortModel> pm) +{ + GraphPortModule* view = GraphPortModule::create(*this, pm); + _views.emplace(pm, view); + view->show(); +} + +void +GraphCanvas::remove_port(SPtr<const PortModel> pm) +{ + auto i = _views.find(pm); + + // Port on this graph + if (i != _views.end()) { + delete i->second; + _views.erase(i); + + } else { + NodeModule* module = dynamic_cast<NodeModule*>(_views[pm->parent()]); + module->delete_port_view(pm); + } + + assert(_views.find(pm) == _views.end()); +} + +Ganv::Port* +GraphCanvas::get_port_view(SPtr<PortModel> port) +{ + Ganv::Module* module = _views[port]; + + // Port on this graph + if (module) { + GraphPortModule* ppm = dynamic_cast<GraphPortModule*>(module); + return ppm + ? *ppm->begin() + : dynamic_cast<Ganv::Port*>(module); + } else { + module = dynamic_cast<NodeModule*>(_views[port->parent()]); + if (module) { + for (const auto& p : *module) { + GUI::Port* pv = dynamic_cast<GUI::Port*>(p); + if (pv && pv->model() == port) { + return pv; + } + } + } + } + + return nullptr; +} + +/** Called when a connection is added to the model. */ +void +GraphCanvas::connection(SPtr<const ArcModel> arc) +{ + Ganv::Port* const tail = get_port_view(arc->tail()); + Ganv::Port* const head = get_port_view(arc->head()); + + if (tail && head) { + new GUI::Arc(*this, arc, tail, head); + } else { + _app.log().error(fmt("Unable to find ports to connect %1% => %2%\n") + % arc->tail_path() % arc->head_path()); + } +} + +/** Called when a connection is removed from the model. */ +void +GraphCanvas::disconnection(SPtr<const ArcModel> arc) +{ + Ganv::Port* const tail = get_port_view(arc->tail()); + Ganv::Port* const head = get_port_view(arc->head()); + + if (tail && head) { + remove_edge_between(tail, head); + if (arc->head()->is_a(_app.uris().lv2_AudioPort)) { + GUI::Port* const h = dynamic_cast<GUI::Port*>(head); + if (h) { + h->activity(_app.forge().make(0.0f)); // Reset peaks + } + } + } else { + _app.log().error(fmt("Unable to find ports to disconnect %1% => %2%\n") + % arc->tail_path() % arc->head_path()); + } +} + +/** Called when the user connects ports on the canvas. */ +void +GraphCanvas::connect(Ganv::Node* tail, Ganv::Node* head) +{ + const GUI::Port* const t = dynamic_cast<GUI::Port*>(tail); + const GUI::Port* const h = dynamic_cast<GUI::Port*>(head); + + if (t && h) { + _app.interface()->connect(t->model()->path(), h->model()->path()); + } +} + +/** Called when the user disconnects ports on the canvas. */ +void +GraphCanvas::disconnect(Ganv::Node* tail, Ganv::Node* head) +{ + const GUI::Port* const t = dynamic_cast<GUI::Port*>(tail); + const GUI::Port* const h = dynamic_cast<GUI::Port*>(head); + + if (t && h) { + _app.interface()->disconnect(t->model()->path(), h->model()->path()); + } +} + +void +GraphCanvas::auto_menu_position(int& x, int& y, bool& push_in) +{ + std::pair<int, int> scroll_offsets; + get_scroll_offsets(scroll_offsets.first, scroll_offsets.second); + + if (_auto_position_count > 1 && scroll_offsets != _auto_position_scroll_offsets) { + // Scroll changed since last auto position, reset + _menu_x = 0; + _menu_y = 0; + _auto_position_count = 0; + } + + if (_menu_x == 0 && _menu_y == 0) { + // No menu position set, start near top left of canvas + widget().translate_coordinates( + *_app.window_factory()->graph_window(_graph), + 64, 64, _menu_x, _menu_y); + + int origin_x; + int origin_y; + widget().get_window()->get_origin(origin_x, origin_y); + _menu_x += origin_x; + _menu_y += origin_y; + } + + const int cascade = _auto_position_count * 32; + + x = _menu_x + cascade; + y = _menu_y + cascade; + push_in = true; + + ++_auto_position_count; + _auto_position_scroll_offsets = scroll_offsets; +} + +bool +GraphCanvas::on_event(GdkEvent* event) +{ + assert(event); + + bool ret = false; + + switch (event->type) { + case GDK_BUTTON_PRESS: + if (event->button.button == 3) { + _auto_position_count = 1; + _menu_x = (int)event->button.x_root; + _menu_y = (int)event->button.y_root; + show_menu(false, event->button.button, event->button.time); + ret = true; + } + break; + + case GDK_KEY_PRESS: + switch (event->key.keyval) { + case GDK_Delete: + destroy_selection(); + ret = true; + break; + case GDK_Home: + scroll_to(0, 0); + break; + case GDK_space: + case GDK_Menu: + show_menu(true, 3, event->key.time); + default: break; + } + + case GDK_MOTION_NOTIFY: + _paste_count = 0; + break; + + default: break; + } + + return ret; +} + +void +GraphCanvas::clear_selection() +{ + GraphWindow* win = _app.window_factory()->graph_window(_graph); + if (win) { + win->set_documentation("", false); + } + + Ganv::Canvas::clear_selection(); +} + +static void +destroy_node(GanvNode* node, void* data) +{ + if (!GANV_IS_MODULE(node)) { + return; + } + + App* app = (App*)data; + Ganv::Module* module = Glib::wrap(GANV_MODULE(node)); + NodeModule* node_module = dynamic_cast<NodeModule*>(module); + + if (node_module) { + app->interface()->del(node_module->block()->uri()); + } else { + GraphPortModule* port_module = dynamic_cast<GraphPortModule*>(module); + if (port_module && + strcmp(port_module->port()->path().symbol(), "control") && + strcmp(port_module->port()->path().symbol(), "notify")) { + app->interface()->del(port_module->port()->uri()); + } + } +} + +static void +destroy_arc(GanvEdge* arc, void* data) +{ + App* app = (App*)data; + Ganv::Edge* arcmm = Glib::wrap(arc); + + Port* tail = dynamic_cast<Port*>(arcmm->get_tail()); + Port* head = dynamic_cast<Port*>(arcmm->get_head()); + app->interface()->disconnect(tail->model()->path(), head->model()->path()); +} + +void +GraphCanvas::destroy_selection() +{ + _app.interface()->bundle_begin(); + for_each_selected_edge(destroy_arc, &_app); + for_each_selected_node(destroy_node, &_app); + _app.interface()->bundle_end(); +} + +static void +serialise_node(GanvNode* node, void* data) +{ + Serialiser* serialiser = (Serialiser*)data; + if (!GANV_IS_MODULE(node)) { + return; + } + + Ganv::Module* module = Glib::wrap(GANV_MODULE(node)); + NodeModule* node_module = dynamic_cast<NodeModule*>(module); + + if (node_module) { + serialiser->serialise(node_module->block()); + } else { + GraphPortModule* port_module = dynamic_cast<GraphPortModule*>(module); + if (port_module) { + serialiser->serialise(port_module->port()); + } + } +} + +static void +serialise_arc(GanvEdge* arc, void* data) +{ + Serialiser* serialiser = (Serialiser*)data; + if (!GANV_IS_EDGE(arc)) { + return; + } + + GUI::Arc* garc = dynamic_cast<GUI::Arc*>(Glib::wrap(GANV_EDGE(arc))); + if (garc) { + serialiser->serialise_arc(Sord::Node(), garc->model()); + } +} + +void +GraphCanvas::copy_selection() +{ + std::lock_guard<std::mutex> lock(_app.world()->rdf_mutex()); + + Serialiser serialiser(*_app.world()); + serialiser.start_to_string(_graph->path(), _graph->base_uri()); + + for_each_selected_node(serialise_node, &serialiser); + for_each_selected_edge(serialise_arc, &serialiser); + + Glib::RefPtr<Gtk::Clipboard> clipboard = Gtk::Clipboard::get(); + clipboard->set_text(serialiser.finish()); + _paste_count = 0; +} + +void +GraphCanvas::paste() +{ + typedef Properties::const_iterator PropIter; + + std::lock_guard<std::mutex> lock(_app.world()->rdf_mutex()); + + const Glib::ustring str = Gtk::Clipboard::get()->wait_for_text(); + SPtr<Parser> parser = _app.loader()->parser(); + const URIs& uris = _app.uris(); + const Raul::Path& parent = _graph->path(); + if (!parser) { + _app.log().error("Unable to load parser, paste unavailable\n"); + return; + } + + // Prepare for paste + clear_selection(); + _pastees.clear(); + ++_paste_count; + + // Make a client store to serve as clipboard + ClientStore clipboard(_app.world()->uris(), _app.log()); + clipboard.set_plugins(_app.store()->plugins()); + clipboard.put(main_uri(), + {{uris.rdf_type, Property(uris.ingen_Graph)}}); + + // Parse clipboard text into clipboard store + boost::optional<URI> base_uri = parser->parse_string( + _app.world(), &clipboard, str, main_uri()); + + // Figure out the copy graph base path + Raul::Path copy_root("/"); + if (base_uri) { + std::string base = *base_uri; + if (base[base.size() - 1] == '/') { + base = base.substr(0, base.size() - 1); + } + copy_root = uri_to_path(URI(base)); + } + + // Find the minimum x and y coordinate of objects to be pasted + float min_x = std::numeric_limits<float>::max(); + float min_y = std::numeric_limits<float>::max(); + for (const auto& c : clipboard) { + if (c.first.parent() == Raul::Path("/")) { + const Atom& x = c.second->get_property(uris.ingen_canvasX); + const Atom& y = c.second->get_property(uris.ingen_canvasY); + if (x.type() == uris.atom_Float) { + min_x = std::min(min_x, x.get<float>()); + } + if (y.type() == uris.atom_Float) { + min_y = std::min(min_y, y.get<float>()); + } + } + } + + // Find canvas paste origin based on pointer position + int widget_point_x, widget_point_y, scroll_x, scroll_y; + widget().get_pointer(widget_point_x, widget_point_y); + get_scroll_offsets(scroll_x, scroll_y); + const int paste_x = widget_point_x + scroll_x + (20.0f * _paste_count); + const int paste_y = widget_point_y + scroll_y + (20.0f * _paste_count); + + _app.interface()->bundle_begin(); + + // Put each top level object in the clipboard store + ClashAvoider avoider(*_app.store().get()); + for (const auto& c : clipboard) { + if (c.first.is_root() || c.first.parent() != Raul::Path("/")) { + continue; + } + + const SPtr<Node> node = c.second; + const Raul::Path& old_path = copy_root.child(node->path()); + const URI& old_uri = path_to_uri(old_path); + const Raul::Path& new_path = avoider.map_path(parent.child(node->path())); + + Properties props{{uris.lv2_prototype, + _app.forge().make_urid(old_uri)}}; + + // Set the same types + const auto t = node->properties().equal_range(uris.rdf_type); + props.insert(t.first, t.second); + + // Set coordinates so paste origin is at the mouse pointer + PropIter xi = node->properties().find(uris.ingen_canvasX); + PropIter yi = node->properties().find(uris.ingen_canvasY); + if (xi != node->properties().end()) { + const float x = xi->second.get<float>() - min_x + paste_x; + props.insert({xi->first, Property(_app.forge().make(x), + xi->second.context())}); + } + if (yi != node->properties().end()) { + const float y = yi->second.get<float>() - min_y + paste_y; + props.insert({yi->first, Property(_app.forge().make(y), + yi->second.context())}); + } + + _app.interface()->put(path_to_uri(new_path), props); + _pastees.insert(new_path); + } + + // Connect objects + for (auto a : clipboard.object(Raul::Path("/"))->arcs()) { + _app.interface()->connect( + avoider.map_path(parent.child(a.second->tail_path())), + avoider.map_path(parent.child(a.second->head_path()))); + } + + _app.interface()->bundle_end(); +} + +void +GraphCanvas::generate_port_name( + const string& sym_base, string& symbol, + const string& name_base, string& name) +{ + symbol = sym_base; + name = name_base; + + char num_buf[5]; + uint32_t i = 1; + for ( ; i < 9999; ++i) { + snprintf(num_buf, sizeof(num_buf), "%u", i); + symbol = sym_base + "_"; + symbol += num_buf; + if (!_graph->get_port(Raul::Symbol::symbolify(symbol))) { + break; + } + } + + assert(Raul::Path::is_valid(string("/") + symbol)); + + name.append(" ").append(num_buf); +} + +void +GraphCanvas::menu_add_port(const string& sym_base, + const string& name_base, + const URI& type, + bool is_output) +{ + string sym, name; + generate_port_name(sym_base, sym, name_base, name); + const Raul::Path& path = _graph->path().child(Raul::Symbol(sym)); + + const URIs& uris = _app.uris(); + + Properties props = get_initial_data(Resource::Graph::INTERNAL); + props.emplace(uris.rdf_type, _app.forge().make_urid(type)); + if (type == uris.atom_AtomPort) { + props.emplace(uris.atom_bufferType, Property(uris.atom_Sequence)); + } + props.emplace( + uris.rdf_type, + Property(is_output ? uris.lv2_OutputPort : uris.lv2_InputPort)); + props.emplace(uris.lv2_index, + _app.forge().make(int32_t(_graph->num_ports()))); + props.emplace(uris.lv2_name, _app.forge().alloc(name.c_str())); + _app.interface()->put(path_to_uri(path), props); +} + +void +GraphCanvas::load_plugin(WPtr<PluginModel> weak_plugin) +{ + SPtr<PluginModel> plugin = weak_plugin.lock(); + if (!plugin) { + return; + } + + Raul::Symbol symbol = plugin->default_block_symbol(); + unsigned offset = _app.store()->child_name_offset(_graph->path(), symbol); + if (offset != 0) { + std::stringstream ss; + ss << symbol << "_" << offset; + symbol = Raul::Symbol(ss.str()); + } + + const URIs& uris = _app.uris(); + const Raul::Path path = _graph->path().child(symbol); + + // FIXME: polyphony? + Properties props = get_initial_data(); + props.emplace(uris.rdf_type, Property(uris.ingen_Block)); + props.emplace(uris.lv2_prototype, uris.forge.make_urid(plugin->uri())); + _app.interface()->put(path_to_uri(path), props); +} + +/** Try to guess a suitable location for a new module. + */ +void +GraphCanvas::get_new_module_location(double& x, double& y) +{ + int scroll_x; + int scroll_y; + get_scroll_offsets(scroll_x, scroll_y); + x = scroll_x + 20; + y = scroll_y + 20; +} + +Properties +GraphCanvas::get_initial_data(Resource::Graph ctx) +{ + Properties result; + const URIs& uris = _app.uris(); + result.emplace(uris.ingen_canvasX, + Property(_app.forge().make((float)_menu_x), ctx)); + result.emplace(uris.ingen_canvasY, + Property(_app.forge().make((float)_menu_y), ctx)); + return result; +} + +void +GraphCanvas::menu_load_plugin() +{ + _app.window_factory()->present_load_plugin(_graph, get_initial_data()); +} + +void +GraphCanvas::menu_load_graph() +{ + _app.window_factory()->present_load_subgraph( + _graph, get_initial_data(Resource::Graph::EXTERNAL)); +} + +void +GraphCanvas::menu_new_graph() +{ + _app.window_factory()->present_new_subgraph( + _graph, get_initial_data(Resource::Graph::EXTERNAL)); +} + +void +GraphCanvas::menu_properties() +{ + _app.window_factory()->present_properties(_graph); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphCanvas.hpp b/src/gui/GraphCanvas.hpp new file mode 100644 index 00000000..a7340744 --- /dev/null +++ b/src/gui/GraphCanvas.hpp @@ -0,0 +1,159 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPHCANVAS_HPP +#define INGEN_GUI_GRAPHCANVAS_HPP + +#include <string> +#include <map> +#include <set> + +#include "lilv/lilv.h" + +#include "ganv/Canvas.hpp" +#include "ganv/Module.hpp" +#include "ingen/Node.hpp" +#include "ingen/client/ArcModel.hpp" +#include "ingen/types.hpp" +#include "raul/Path.hpp" + +#include "NodeModule.hpp" + +namespace Ingen { + +namespace Client { class GraphModel; } + +namespace GUI { + +class NodeModule; +class PluginMenu; + +/** Graph canvas widget. + * + * \ingroup GUI + */ +class GraphCanvas : public Ganv::Canvas +{ +public: + GraphCanvas(App& app, + SPtr<const Client::GraphModel> graph, + int width, + int height); + + virtual ~GraphCanvas() {} + + App& app() { return _app; } + + void build(); + void show_human_names(bool b); + void show_port_names(bool b); + bool show_port_names() const { return _show_port_names; } + + void add_plugin(SPtr<Client::PluginModel> p); + void remove_plugin(const URI& uri); + void add_block(SPtr<const Client::BlockModel> bm); + void remove_block(SPtr<const Client::BlockModel> bm); + void add_port(SPtr<const Client::PortModel> pm); + void remove_port(SPtr<const Client::PortModel> pm); + void connection(SPtr<const Client::ArcModel> arc); + void disconnection(SPtr<const Client::ArcModel> arc); + + void get_new_module_location(double& x, double& y); + + void clear_selection(); + void destroy_selection(); + void copy_selection(); + void paste(); + + void show_menu(bool position, unsigned button, uint32_t time); + + bool on_event(GdkEvent* event); + +private: + enum class ControlType { NUMBER, BUTTON }; + void generate_port_name( + const std::string& sym_base, std::string& symbol, + const std::string& name_base, std::string& name); + + void menu_add_port(const std::string& sym_base, + const std::string& name_base, + const URI& type, + bool is_output); + + void menu_load_plugin(); + void menu_new_graph(); + void menu_load_graph(); + void menu_properties(); + void load_plugin(WPtr<Client::PluginModel> weak_plugin); + + void build_menus(); + + void auto_menu_position(int& x, int& y, bool& push_in); + + typedef std::multimap<const std::string, const LilvPluginClass*> LV2Children; + + Properties get_initial_data(Resource::Graph ctx=Resource::Graph::DEFAULT); + + Ganv::Port* get_port_view(SPtr<Client::PortModel> port); + + void connect(Ganv::Node* tail, + Ganv::Node* head); + + void disconnect(Ganv::Node* tail, + Ganv::Node* head); + + App& _app; + SPtr<const Client::GraphModel> _graph; + + typedef std::map<SPtr<const Client::ObjectModel>, Ganv::Module*> Views; + Views _views; + + int _auto_position_count; + std::pair<int, int> _auto_position_scroll_offsets; + + int _menu_x; + int _menu_y; + int _paste_count; + + // Track pasted objects so they can be selected when they arrive + std::set<Raul::Path> _pastees; + + Gtk::Menu* _menu; + Gtk::Menu* _internal_menu; + PluginMenu* _plugin_menu; + Gtk::MenuItem* _menu_add_audio_input; + Gtk::MenuItem* _menu_add_audio_output; + Gtk::MenuItem* _menu_add_control_input; + Gtk::MenuItem* _menu_add_control_output; + Gtk::MenuItem* _menu_add_cv_input; + Gtk::MenuItem* _menu_add_cv_output; + Gtk::MenuItem* _menu_add_event_input; + Gtk::MenuItem* _menu_add_event_output; + Gtk::MenuItem* _menu_load_plugin; + Gtk::MenuItem* _menu_load_graph; + Gtk::MenuItem* _menu_new_graph; + Gtk::MenuItem* _menu_properties; + Gtk::CheckMenuItem* _menu_edit; + + bool _human_names; + bool _show_port_names; + bool _menu_dirty; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPHCANVAS_HPP diff --git a/src/gui/GraphPortModule.cpp b/src/gui/GraphPortModule.cpp new file mode 100644 index 00000000..5987b0e3 --- /dev/null +++ b/src/gui/GraphPortModule.cpp @@ -0,0 +1,166 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> +#include <utility> + +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "Style.hpp" +#include "GraphCanvas.hpp" +#include "GraphPortModule.hpp" +#include "GraphWindow.hpp" +#include "Port.hpp" +#include "PortMenu.hpp" +#include "RenameWindow.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +GraphPortModule::GraphPortModule(GraphCanvas& canvas, + SPtr<const Client::PortModel> model) + : Ganv::Module(canvas, "", 0, 0, false) // FIXME: coords? + , _model(model) + , _port(nullptr) +{ + assert(model); + + assert(dynamic_ptr_cast<const GraphModel>(model->parent())); + + set_stacked(model->polyphonic()); + if (model->is_input() && !model->is_numeric()) { + set_is_source(true); + } + + model->signal_property().connect( + sigc::mem_fun(this, &GraphPortModule::property_changed)); + + signal_moved().connect( + sigc::mem_fun(this, &GraphPortModule::store_location)); +} + +GraphPortModule* +GraphPortModule::create(GraphCanvas& canvas, + SPtr<const PortModel> model) +{ + GraphPortModule* ret = new GraphPortModule(canvas, model); + Port* port = Port::create(canvas.app(), *ret, model, true); + + ret->set_port(port); + if (model->is_numeric()) { + port->show_control(); + } + + for (const auto& p : model->properties()) { + ret->property_changed(p.first, p.second); + } + + return ret; +} + +App& +GraphPortModule::app() const +{ + return ((GraphCanvas*)canvas())->app(); +} + +bool +GraphPortModule::show_menu(GdkEventButton* ev) +{ + return _port->show_menu(ev); +} + +void +GraphPortModule::store_location(double ax, double ay) +{ + const URIs& uris = app().uris(); + + const Atom x(app().forge().make(static_cast<float>(ax))); + const Atom y(app().forge().make(static_cast<float>(ay))); + + if (x != _model->get_property(uris.ingen_canvasX) || + y != _model->get_property(uris.ingen_canvasY)) + { + app().interface()->put( + _model->uri(), + {{uris.ingen_canvasX, Property(x, Property::Graph::INTERNAL)}, + {uris.ingen_canvasY, Property(y, Property::Graph::INTERNAL)}}); + } +} + +void +GraphPortModule::show_human_names(bool b) +{ + const URIs& uris = app().uris(); + const Atom& name = _model->get_property(uris.lv2_name); + if (b && name.type() == uris.forge.String) { + set_name(name.ptr<char>()); + } else { + set_name(_model->symbol().c_str()); + } +} + +void +GraphPortModule::set_name(const std::string& n) +{ + _port->set_label(n.c_str()); +} + +void +GraphPortModule::property_changed(const URI& key, const Atom& value) +{ + const URIs& uris = app().uris(); + if (value.type() == uris.forge.Float) { + if (key == uris.ingen_canvasX) { + move_to(value.get<float>(), get_y()); + } else if (key == uris.ingen_canvasY) { + move_to(get_x(), value.get<float>()); + } + } else if (value.type() == uris.forge.String) { + if (key == uris.lv2_name && + app().world()->conf().option("human-names").get<int32_t>()) { + set_name(value.ptr<char>()); + } else if (key == uris.lv2_symbol && + !app().world()->conf().option("human-names").get<int32_t>()) { + set_name(value.ptr<char>()); + } + } else if (value.type() == uris.forge.Bool) { + if (key == uris.ingen_polyphonic) { + set_stacked(value.get<int32_t>()); + } + } +} + +void +GraphPortModule::set_selected(gboolean b) +{ + if (b != get_selected()) { + Module::set_selected(b); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphPortModule.hpp b/src/gui/GraphPortModule.hpp new file mode 100644 index 00000000..97bc2e5b --- /dev/null +++ b/src/gui/GraphPortModule.hpp @@ -0,0 +1,79 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPHPORTMODULE_HPP +#define INGEN_GUI_GRAPHPORTMODULE_HPP + +#include <string> + +#include "ganv/Module.hpp" + +#include "Port.hpp" + +namespace Raul { class Atom; } + +namespace Ingen { namespace Client { +class PortModel; +} } + +namespace Ingen { +namespace GUI { + +class GraphCanvas; +class Port; +class PortMenu; + +/** A "module" to represent a graph's port on its own canvas. + * + * Translation: This is the nameless single port pseudo module thingy. + * + * \ingroup GUI + */ +class GraphPortModule : public Ganv::Module +{ +public: + static GraphPortModule* create( + GraphCanvas& canvas, + SPtr<const Client::PortModel> model); + + App& app() const; + + virtual void store_location(double ax, double ay); + void show_human_names(bool b); + + void set_name(const std::string& n); + + SPtr<const Client::PortModel> port() const { return _model; } + +protected: + GraphPortModule(GraphCanvas& canvas, + SPtr<const Client::PortModel> model); + + bool show_menu(GdkEventButton* ev); + void set_selected(gboolean b); + + void set_port(Port* port) { _port = port; } + + void property_changed(const URI& key, const Atom& value); + + SPtr<const Client::PortModel> _model; + Port* _port; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPHPORTMODULE_HPP diff --git a/src/gui/GraphTreeWindow.cpp b/src/gui/GraphTreeWindow.cpp new file mode 100644 index 00000000..1eb6557b --- /dev/null +++ b/src/gui/GraphTreeWindow.cpp @@ -0,0 +1,235 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "App.hpp" +#include "GraphTreeWindow.hpp" +#include "SubgraphModule.hpp" +#include "WindowFactory.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "raul/Path.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +GraphTreeWindow::GraphTreeWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) + , _app(nullptr) + , _enable_signal(true) +{ + xml->get_widget_derived("graphs_treeview", _graphs_treeview); + + _graph_treestore = Gtk::TreeStore::create(_graph_tree_columns); + _graphs_treeview->set_window(this); + _graphs_treeview->set_model(_graph_treestore); + Gtk::TreeViewColumn* name_col = Gtk::manage( + new Gtk::TreeViewColumn("Graph", _graph_tree_columns.name_col)); + Gtk::TreeViewColumn* enabled_col = Gtk::manage( + new Gtk::TreeViewColumn("Run", _graph_tree_columns.enabled_col)); + name_col->set_resizable(true); + name_col->set_expand(true); + + _graphs_treeview->append_column(*name_col); + _graphs_treeview->append_column(*enabled_col); + Gtk::CellRendererToggle* enabled_renderer = dynamic_cast<Gtk::CellRendererToggle*>( + _graphs_treeview->get_column_cell_renderer(1)); + enabled_renderer->property_activatable() = true; + + _graph_tree_selection = _graphs_treeview->get_selection(); + + _graphs_treeview->signal_row_activated().connect( + sigc::mem_fun(this, &GraphTreeWindow::event_graph_activated)); + enabled_renderer->signal_toggled().connect( + sigc::mem_fun(this, &GraphTreeWindow::event_graph_enabled_toggled)); + + _graphs_treeview->columns_autosize(); +} + +void +GraphTreeWindow::init(App& app, ClientStore& store) +{ + _app = &app; + store.signal_new_object().connect( + sigc::mem_fun(this, &GraphTreeWindow::new_object)); +} + +void +GraphTreeWindow::new_object(SPtr<ObjectModel> object) +{ + SPtr<GraphModel> graph = dynamic_ptr_cast<GraphModel>(object); + if (graph) { + add_graph(graph); + } +} + +void +GraphTreeWindow::add_graph(SPtr<GraphModel> pm) +{ + if (!pm->parent()) { + Gtk::TreeModel::iterator iter = _graph_treestore->append(); + Gtk::TreeModel::Row row = *iter; + if (pm->path().is_root()) { + row[_graph_tree_columns.name_col] = _app->interface()->uri().string(); + } else { + row[_graph_tree_columns.name_col] = pm->symbol().c_str(); + } + row[_graph_tree_columns.enabled_col] = pm->enabled(); + row[_graph_tree_columns.graph_model_col] = pm; + _graphs_treeview->expand_row(_graph_treestore->get_path(iter), true); + } else { + Gtk::TreeModel::Children children = _graph_treestore->children(); + Gtk::TreeModel::iterator c = find_graph(children, pm->parent()); + + if (c != children.end()) { + Gtk::TreeModel::iterator iter = _graph_treestore->append(c->children()); + Gtk::TreeModel::Row row = *iter; + row[_graph_tree_columns.name_col] = pm->symbol().c_str(); + row[_graph_tree_columns.enabled_col] = pm->enabled(); + row[_graph_tree_columns.graph_model_col] = pm; + _graphs_treeview->expand_row(_graph_treestore->get_path(iter), true); + } + } + + pm->signal_property().connect( + sigc::bind(sigc::mem_fun(this, &GraphTreeWindow::graph_property_changed), + pm)); + + pm->signal_moved().connect( + sigc::bind(sigc::mem_fun(this, &GraphTreeWindow::graph_moved), + pm)); + + pm->signal_destroyed().connect( + sigc::bind(sigc::mem_fun(this, &GraphTreeWindow::remove_graph), + pm)); +} + +void +GraphTreeWindow::remove_graph(SPtr<GraphModel> pm) +{ + Gtk::TreeModel::iterator i = find_graph(_graph_treestore->children(), pm); + if (i != _graph_treestore->children().end()) { + _graph_treestore->erase(i); + } +} + +Gtk::TreeModel::iterator +GraphTreeWindow::find_graph(Gtk::TreeModel::Children root, + SPtr<Client::ObjectModel> graph) +{ + for (Gtk::TreeModel::iterator c = root.begin(); c != root.end(); ++c) { + SPtr<GraphModel> pm = (*c)[_graph_tree_columns.graph_model_col]; + if (graph == pm) { + return c; + } else if ((*c)->children().size() > 0) { + Gtk::TreeModel::iterator ret = find_graph(c->children(), graph); + if (ret != c->children().end()) { + return ret; + } + } + } + return root.end(); +} + +/** Show the context menu for the selected graph in the graphs treeview. + */ +void +GraphTreeWindow::show_graph_menu(GdkEventButton* ev) +{ + Gtk::TreeModel::iterator active = _graph_tree_selection->get_selected(); + if (active) { + Gtk::TreeModel::Row row = *active; + SPtr<GraphModel> pm = row[_graph_tree_columns.graph_model_col]; + if (pm) { + _app->log().warn("TODO: graph menu from tree window"); + } + } +} + +void +GraphTreeWindow::event_graph_activated(const Gtk::TreeModel::Path& path, + Gtk::TreeView::Column* col) +{ + Gtk::TreeModel::iterator active = _graph_treestore->get_iter(path); + Gtk::TreeModel::Row row = *active; + SPtr<GraphModel> pm = row[_graph_tree_columns.graph_model_col]; + + _app->window_factory()->present_graph(pm); +} + +void +GraphTreeWindow::event_graph_enabled_toggled(const Glib::ustring& path_str) +{ + Gtk::TreeModel::Path path(path_str); + Gtk::TreeModel::iterator active = _graph_treestore->get_iter(path); + Gtk::TreeModel::Row row = *active; + + SPtr<GraphModel> pm = row[_graph_tree_columns.graph_model_col]; + assert(pm); + + if (_enable_signal) { + _app->set_property(pm->uri(), + _app->uris().ingen_enabled, + _app->forge().make((bool)!pm->enabled())); + } +} + +void +GraphTreeWindow::graph_property_changed(const URI& key, + const Atom& value, + SPtr<GraphModel> graph) +{ + const URIs& uris = _app->uris(); + _enable_signal = false; + if (key == uris.ingen_enabled && value.type() == uris.forge.Bool) { + Gtk::TreeModel::iterator i = find_graph(_graph_treestore->children(), graph); + if (i != _graph_treestore->children().end()) { + Gtk::TreeModel::Row row = *i; + row[_graph_tree_columns.enabled_col] = value.get<int32_t>(); + } else { + _app->log().error(fmt("Unable to find graph %1%\n") + % graph->path()); + } + } + _enable_signal = true; +} + +void +GraphTreeWindow::graph_moved(SPtr<GraphModel> graph) +{ + _enable_signal = false; + + Gtk::TreeModel::iterator i + = find_graph(_graph_treestore->children(), graph); + + if (i != _graph_treestore->children().end()) { + Gtk::TreeModel::Row row = *i; + row[_graph_tree_columns.name_col] = graph->symbol().c_str(); + } else { + _app->log().error(fmt("Unable to find graph %1%\n") + % graph->path()); + } + + _enable_signal = true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphTreeWindow.hpp b/src/gui/GraphTreeWindow.hpp new file mode 100644 index 00000000..005f39a8 --- /dev/null +++ b/src/gui/GraphTreeWindow.hpp @@ -0,0 +1,123 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPHTREEWINDOW_HPP +#define INGEN_GUI_GRAPHTREEWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treestore.h> +#include <gtkmm/treeview.h> + +#include "Window.hpp" + +namespace Raul { class Path; } + +namespace Ingen { + +namespace Client { class ClientStore; class ObjectModel; } + +namespace GUI { + +class GraphWindow; +class GraphTreeView; + +/** Window with a TreeView of all loaded graphs. + * + * \ingroup GUI + */ +class GraphTreeWindow : public Window +{ +public: + GraphTreeWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app, Client::ClientStore& store); + + void new_object(SPtr<Client::ObjectModel> object); + + void graph_property_changed(const URI& key, + const Atom& value, + SPtr<Client::GraphModel> graph); + + void graph_moved(SPtr<Client::GraphModel> graph); + + void add_graph(SPtr<Client::GraphModel> pm); + void remove_graph(SPtr<Client::GraphModel> pm); + void show_graph_menu(GdkEventButton* ev); + +protected: + void event_graph_activated(const Gtk::TreeModel::Path& path, + Gtk::TreeView::Column* col); + + void event_graph_enabled_toggled(const Glib::ustring& path_str); + + Gtk::TreeModel::iterator find_graph( + Gtk::TreeModel::Children root, + SPtr<Client::ObjectModel> graph); + + GraphTreeView* _graphs_treeview; + + struct GraphTreeModelColumns : public Gtk::TreeModel::ColumnRecord + { + GraphTreeModelColumns() { + add(name_col); + add(enabled_col); + add(graph_model_col); + } + + Gtk::TreeModelColumn<Glib::ustring> name_col; + Gtk::TreeModelColumn<bool> enabled_col; + Gtk::TreeModelColumn<SPtr<Client::GraphModel> > graph_model_col; + }; + + App* _app; + GraphTreeModelColumns _graph_tree_columns; + Glib::RefPtr<Gtk::TreeStore> _graph_treestore; + Glib::RefPtr<Gtk::TreeSelection> _graph_tree_selection; + bool _enable_signal; +}; + +/** Derived TreeView class to support context menus for graphs */ +class GraphTreeView : public Gtk::TreeView +{ +public: + GraphTreeView(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::TreeView(cobject) + , _window(NULL) + {} + + void set_window(GraphTreeWindow* win) { _window = win; } + + bool on_button_press_event(GdkEventButton* ev) { + bool ret = Gtk::TreeView::on_button_press_event(ev); + + if ((ev->type == GDK_BUTTON_PRESS) && (ev->button == 3)) + _window->show_graph_menu(ev); + + return ret; + } + +private: + GraphTreeWindow* _window; + +}; // struct GraphTreeView + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPHTREEWINDOW_HPP diff --git a/src/gui/GraphView.cpp b/src/gui/GraphView.cpp new file mode 100644 index 00000000..e6361249 --- /dev/null +++ b/src/gui/GraphView.cpp @@ -0,0 +1,154 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <fstream> + +#include "ingen/Interface.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "LoadPluginWindow.hpp" +#include "NewSubgraphWindow.hpp" +#include "GraphCanvas.hpp" +#include "GraphTreeWindow.hpp" +#include "GraphView.hpp" +#include "WidgetFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +GraphView::GraphView(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::Box(cobject) + , _app(nullptr) + , _breadcrumb_container(nullptr) + , _enable_signal(true) +{ + property_visible() = false; + + xml->get_widget("graph_view_breadcrumb_container", _breadcrumb_container); + xml->get_widget("graph_view_toolbar", _toolbar); + xml->get_widget("graph_view_process_but", _process_but); + xml->get_widget("graph_view_poly_spin", _poly_spin); + xml->get_widget("graph_view_scrolledwindow", _canvas_scrolledwindow); + + _toolbar->set_toolbar_style(Gtk::TOOLBAR_ICONS); + _canvas_scrolledwindow->property_hadjustment().get_value()->set_step_increment(10); + _canvas_scrolledwindow->property_vadjustment().get_value()->set_step_increment(10); +} + +GraphView::~GraphView() +{ + _canvas_scrolledwindow->remove(); +} + +void +GraphView::init(App& app) +{ + _app = &app; +} + +void +GraphView::set_graph(SPtr<const GraphModel> graph) +{ + assert(!_canvas); // FIXME: remove + + assert(_breadcrumb_container); // ensure created + + _graph = graph; + _canvas = SPtr<GraphCanvas>(new GraphCanvas(*_app, graph, 1600*2, 1200*2)); + _canvas->build(); + + _canvas_scrolledwindow->add(_canvas->widget()); + + _poly_spin->set_range(1, 128); + _poly_spin->set_increments(1, 4); + _poly_spin->set_value(graph->internal_poly()); + + for (const auto& p : graph->properties()) { + property_changed(p.first, p.second); + } + + // Connect model signals to track state + graph->signal_property().connect( + sigc::mem_fun(this, &GraphView::property_changed)); + + // Connect widget signals to do things + _process_but->signal_toggled().connect( + sigc::mem_fun(this, &GraphView::process_toggled)); + + _poly_spin->signal_value_changed().connect( + sigc::mem_fun(*this, &GraphView::poly_changed)); + + _canvas->widget().grab_focus(); +} + +SPtr<GraphView> +GraphView::create(App& app, SPtr<const GraphModel> graph) +{ + GraphView* result = nullptr; + Glib::RefPtr<Gtk::Builder> xml = WidgetFactory::create("warehouse_win"); + xml->get_widget_derived("graph_view_box", result); + result->init(app); + result->set_graph(graph); + return SPtr<GraphView>(result); +} + +void +GraphView::process_toggled() +{ + if (!_enable_signal) { + return; + } + + _app->set_property(_graph->uri(), + _app->uris().ingen_enabled, + _app->forge().make((bool)_process_but->get_active())); +} + +void +GraphView::poly_changed() +{ + const int poly = _poly_spin->get_value_as_int(); + if (_enable_signal && poly != (int)_graph->internal_poly()) { + _app->set_property(_graph->uri(), + _app->uris().ingen_polyphony, + _app->forge().make(poly)); + } +} + +void +GraphView::property_changed(const URI& predicate, const Atom& value) +{ + _enable_signal = false; + if (predicate == _app->uris().ingen_enabled) { + if (value.type() == _app->uris().forge.Bool) { + _process_but->set_active(value.get<int32_t>()); + } + } else if (predicate == _app->uris().ingen_polyphony) { + if (value.type() == _app->uris().forge.Int) { + _poly_spin->set_value(value.get<int32_t>()); + } + } + _enable_signal = true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphView.hpp b/src/gui/GraphView.hpp new file mode 100644 index 00000000..03569831 --- /dev/null +++ b/src/gui/GraphView.hpp @@ -0,0 +1,98 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPHVIEW_HPP +#define INGEN_GUI_GRAPHVIEW_HPP + +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/spinbutton.h> +#include <gtkmm/toggletoolbutton.h> +#include <gtkmm/toolbar.h> +#include <gtkmm/toolitem.h> +#include <gtkmm/toolitem.h> + +#include "ingen/types.hpp" + +namespace Raul { class Atom; } + +namespace Ingen { + +namespace Client { +class PortModel; +class MetadataModel; +class GraphModel; +class ObjectModel; +} + +namespace GUI { + +class App; +class LoadPluginWindow; +class NewSubgraphWindow; +class GraphCanvas; +class GraphDescriptionWindow; +class SubgraphModule; + +/** The graph specific contents of a GraphWindow (ie the canvas and whatever else). + * + * \ingroup GUI + */ +class GraphView : public Gtk::Box +{ +public: + GraphView(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + ~GraphView(); + + void init(App& app); + + SPtr<GraphCanvas> canvas() const { return _canvas; } + SPtr<const Client::GraphModel> graph() const { return _graph; } + Gtk::ToolItem* breadcrumb_container() const { return _breadcrumb_container; } + + static SPtr<GraphView> create(App& app, + SPtr<const Client::GraphModel> graph); + +private: + void set_graph(SPtr<const Client::GraphModel> graph); + + void process_toggled(); + void poly_changed(); + void clear_clicked(); + + void property_changed(const URI& predicate, const Atom& value); + + App* _app; + + SPtr<const Client::GraphModel> _graph; + SPtr<GraphCanvas> _canvas; + + Gtk::ScrolledWindow* _canvas_scrolledwindow; + Gtk::Toolbar* _toolbar; + Gtk::ToggleToolButton* _process_but; + Gtk::SpinButton* _poly_spin; + Gtk::ToolItem* _breadcrumb_container; + + bool _enable_signal; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPHVIEW_HPP diff --git a/src/gui/GraphWindow.cpp b/src/gui/GraphWindow.cpp new file mode 100644 index 00000000..b5a89c79 --- /dev/null +++ b/src/gui/GraphWindow.cpp @@ -0,0 +1,85 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "GraphCanvas.hpp" +#include "GraphView.hpp" +#include "GraphWindow.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { +namespace GUI { + +GraphWindow::GraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) + , _box(nullptr) + , _position_stored(false) + , _x(0) + , _y(0) +{ + property_visible() = false; + + xml->get_widget_derived("graph_win_vbox", _box); + + set_title("Ingen"); +} + +GraphWindow::~GraphWindow() +{ + delete _box; +} + +void +GraphWindow::init_window(App& app) +{ + Window::init_window(app); + _box->init_box(app); + _box->set_window(this); +} + +void +GraphWindow::on_show() +{ + if (_position_stored) { + move(_x, _y); + } + + Gtk::Window::on_show(); + + _box->view()->canvas()->widget().grab_focus(); +} + +void +GraphWindow::on_hide() +{ + _position_stored = true; + get_position(_x, _y); + Gtk::Window::on_hide(); +} + +bool +GraphWindow::on_key_press_event(GdkEventKey* event) +{ + // Disable Window C-w handling so quit works correctly + return Gtk::Window::on_key_press_event(event); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/GraphWindow.hpp b/src/gui/GraphWindow.hpp new file mode 100644 index 00000000..b4e51d7b --- /dev/null +++ b/src/gui/GraphWindow.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GRAPH_WINDOW_HPP +#define INGEN_GUI_GRAPH_WINDOW_HPP + +#include <string> + +#include <gtkmm/builder.h> + +#include "ingen/types.hpp" + +#include "GraphBox.hpp" +#include "Window.hpp" + +namespace Ingen { + +namespace Client { +class GraphModel; +} + +namespace GUI { + +/** A window for a graph. + * + * \ingroup GUI + */ +class GraphWindow : public Window +{ +public: + GraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + ~GraphWindow(); + + void init_window(App& app); + + SPtr<const Client::GraphModel> graph() const { return _box->graph(); } + GraphBox* box() const { return _box; } + + bool documentation_is_visible() { return _box->documentation_is_visible(); } + + void set_documentation(const std::string& doc, bool html) { + _box->set_documentation(doc, html); + } + + void show_port_status(const Client::PortModel* model, + const Atom& value) { + _box->show_port_status(model, value); + } + +protected: + void on_hide(); + void on_show(); + bool on_key_press_event(GdkEventKey* event); + +private: + GraphBox* _box; + bool _position_stored; + int _x; + int _y; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GRAPH_WINDOW_HPP diff --git a/src/gui/LoadGraphWindow.cpp b/src/gui/LoadGraphWindow.cpp new file mode 100644 index 00000000..b02ca510 --- /dev/null +++ b/src/gui/LoadGraphWindow.cpp @@ -0,0 +1,257 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <list> +#include <string> + +#include <boost/optional.hpp> +#include <glibmm/miscutils.h> + +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/runtime_paths.hpp" + +#include "App.hpp" +#include "GraphView.hpp" +#include "LoadGraphWindow.hpp" +#include "Style.hpp" +#include "ThreadedLoader.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +LoadGraphWindow::LoadGraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::FileChooserDialog(cobject) + , _app(nullptr) + , _merge_ports(false) +{ + xml->get_widget("load_graph_symbol_label", _symbol_label); + xml->get_widget("load_graph_symbol_entry", _symbol_entry); + xml->get_widget("load_graph_ports_label", _ports_label); + xml->get_widget("load_graph_merge_ports_radio", _merge_ports_radio); + xml->get_widget("load_graph_insert_ports_radio", _insert_ports_radio); + xml->get_widget("load_graph_poly_voices_radio", _poly_voices_radio); + xml->get_widget("load_graph_poly_from_file_radio", _poly_from_file_radio); + xml->get_widget("load_graph_poly_spinbutton", _poly_spinbutton); + xml->get_widget("load_graph_ok_button", _ok_button); + xml->get_widget("load_graph_cancel_button", _cancel_button); + + _cancel_button->signal_clicked().connect( + sigc::mem_fun(this, &LoadGraphWindow::cancel_clicked)); + _ok_button->signal_clicked().connect( + sigc::mem_fun(this, &LoadGraphWindow::ok_clicked)); + _merge_ports_radio->signal_toggled().connect( + sigc::mem_fun(this, &LoadGraphWindow::merge_ports_selected)); + _insert_ports_radio->signal_toggled().connect( + sigc::mem_fun(this, &LoadGraphWindow::insert_ports_selected)); + _poly_from_file_radio->signal_toggled().connect( + sigc::bind(sigc::mem_fun(_poly_spinbutton, &Gtk::SpinButton::set_sensitive), + false)); + _poly_voices_radio->signal_toggled().connect( + sigc::bind(sigc::mem_fun(_poly_spinbutton, &Gtk::SpinButton::set_sensitive), + true)); + + signal_selection_changed().connect( + sigc::mem_fun(this, &LoadGraphWindow::selection_changed)); + + Gtk::FileFilter file_filter; + file_filter.add_pattern("*.ttl"); + file_filter.set_name("Ingen graph files (*.ttl)"); + add_filter(file_filter); + + Gtk::FileFilter bundle_filter; + bundle_filter.add_pattern("*.ingen"); + bundle_filter.set_name("Ingen bundles (*.ingen)"); + add_filter(bundle_filter); + + property_select_multiple() = true; + + // Add global examples directory to "shortcut folders" (bookmarks) + const FilePath examples_dir = Ingen::data_file_path("graphs"); + if (Glib::file_test(examples_dir, Glib::FILE_TEST_IS_DIR)) { + add_shortcut_folder(examples_dir.string()); + } +} + +void +LoadGraphWindow::present(SPtr<const GraphModel> graph, + bool import, + Properties data) +{ + _import = import; + set_graph(graph); + _symbol_label->property_visible() = !import; + _symbol_entry->property_visible() = !import; + _ports_label->property_visible() = _import; + _merge_ports_radio->property_visible() = _import; + _insert_ports_radio->property_visible() = _import; + _initial_data = data; + Gtk::Window::present(); +} + +/** Sets the graph model for this window and initializes everything. + * + * This function MUST be called before using the window in any way! + */ +void +LoadGraphWindow::set_graph(SPtr<const GraphModel> graph) +{ + _graph = graph; + _symbol_entry->set_text(""); + _symbol_entry->set_sensitive(!_import); + _poly_spinbutton->set_value(graph->internal_poly()); +} + +void +LoadGraphWindow::on_show() +{ + const Atom& dir = _app->world()->conf().option("graph-directory"); + if (dir.is_valid()) { + set_current_folder(dir.ptr<char>()); + } + Gtk::FileChooserDialog::on_show(); +} + +void +LoadGraphWindow::merge_ports_selected() +{ + _merge_ports = true; +} + +void +LoadGraphWindow::insert_ports_selected() +{ + _merge_ports = false; +} + +void +LoadGraphWindow::ok_clicked() +{ + if (!_graph) { + hide(); + return; + } + + const URIs& uris = _app->uris(); + + if (_poly_voices_radio->get_active()) { + _initial_data.emplace( + uris.ingen_polyphony, + _app->forge().make(_poly_spinbutton->get_value_as_int())); + } + + if (get_uri() == "") { + return; + } + + if (_import) { + // If unset load_graph will load value + boost::optional<Raul::Path> parent; + boost::optional<Raul::Symbol> symbol; + if (!_graph->path().is_root()) { + parent = _graph->path().parent(); + symbol = _graph->symbol(); + } + + _app->loader()->load_graph( + true, FilePath(get_filename()), parent, symbol, _initial_data); + + } else { + std::list<Glib::ustring> uri_list = get_filenames(); + for (auto u : uri_list) { + // Cascade + Atom& x = _initial_data.find(uris.ingen_canvasX)->second; + x = _app->forge().make(x.get<float>() + 20.0f); + Atom& y = _initial_data.find(uris.ingen_canvasY)->second; + y = _app->forge().make(y.get<float>() + 20.0f); + + Raul::Symbol symbol(symbol_from_filename(u)); + if (uri_list.size() == 1 && _symbol_entry->get_text() != "") { + symbol = Raul::Symbol::symbolify(_symbol_entry->get_text()); + } + + symbol = avoid_symbol_clash(symbol); + + _app->loader()->load_graph( + false, FilePath(URI(u).path()), _graph->path(), symbol, _initial_data); + } + } + + _graph.reset(); + hide(); + + _app->world()->conf().set( + "graph-directory", + _app->world()->forge().alloc(get_current_folder())); +} + +void +LoadGraphWindow::cancel_clicked() +{ + _graph.reset(); + hide(); +} + +Raul::Symbol +LoadGraphWindow::symbol_from_filename(const Glib::ustring& filename) +{ + std::string symbol_str = Glib::path_get_basename(get_filename()); + symbol_str = symbol_str.substr(0, symbol_str.find('.')); + return Raul::Symbol::symbolify(symbol_str); +} + +Raul::Symbol +LoadGraphWindow::avoid_symbol_clash(const Raul::Symbol& symbol) +{ + unsigned offset = _app->store()->child_name_offset( + _graph->path(), symbol); + + if (offset != 0) { + std::stringstream ss; + ss << symbol << "_" << offset; + return Raul::Symbol(ss.str()); + } else { + return symbol; + } +} + +void +LoadGraphWindow::selection_changed() +{ + if (_import) { + return; + } + + if (get_filenames().size() != 1) { + _symbol_entry->set_text(""); + _symbol_entry->set_sensitive(false); + } else { + _symbol_entry->set_text( + avoid_symbol_clash(symbol_from_filename(get_filename())).c_str()); + _symbol_entry->set_sensitive(true); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/LoadGraphWindow.hpp b/src/gui/LoadGraphWindow.hpp new file mode 100644 index 00000000..8ec5ed4b --- /dev/null +++ b/src/gui/LoadGraphWindow.hpp @@ -0,0 +1,95 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_LOADGRAPHWINDOW_HPP +#define INGEN_GUI_LOADGRAPHWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/filechooserdialog.h> +#include <gtkmm/label.h> +#include <gtkmm/radiobutton.h> +#include <gtkmm/spinbutton.h> + +#include "ingen/Node.hpp" +#include "ingen/types.hpp" + +namespace Ingen { + +namespace Client { class GraphModel; } + +namespace GUI { + +/** 'Load Graph' Window. + * + * Loaded from XML as a derived object. + * + * \ingroup GUI + */ +class LoadGraphWindow : public Gtk::FileChooserDialog +{ +public: + LoadGraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app) { _app = &app; } + + void set_graph(SPtr<const Client::GraphModel> graph); + + void present(SPtr<const Client::GraphModel> graph, + bool import, + Properties data); + +protected: + void on_show(); + +private: + void merge_ports_selected(); + void insert_ports_selected(); + + void selection_changed(); + void cancel_clicked(); + void ok_clicked(); + + Raul::Symbol symbol_from_filename(const Glib::ustring& filename); + Raul::Symbol avoid_symbol_clash(const Raul::Symbol& symbol); + + App* _app; + + Properties _initial_data; + + SPtr<const Client::GraphModel> _graph; + + Gtk::Label* _symbol_label; + Gtk::Entry* _symbol_entry; + Gtk::Label* _ports_label; + Gtk::RadioButton* _merge_ports_radio; + Gtk::RadioButton* _insert_ports_radio; + Gtk::RadioButton* _poly_voices_radio; + Gtk::RadioButton* _poly_from_file_radio; + Gtk::SpinButton* _poly_spinbutton; + Gtk::Button* _ok_button; + Gtk::Button* _cancel_button; + + bool _import; + bool _merge_ports; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_LOADGRAPHWINDOW_HPP diff --git a/src/gui/LoadPluginWindow.cpp b/src/gui/LoadPluginWindow.cpp new file mode 100644 index 00000000..c96634cc --- /dev/null +++ b/src/gui/LoadPluginWindow.cpp @@ -0,0 +1,515 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include <stddef.h> + +#include <cassert> +#include <algorithm> + +#include "ingen/Interface.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "LoadPluginWindow.hpp" +#include "GraphCanvas.hpp" +#include "GraphView.hpp" +#include "GraphWindow.hpp" + +#include "ingen_config.h" + +using std::string; + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +LoadPluginWindow::LoadPluginWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) + , _name_offset(0) + , _has_shown(false) + , _refresh_list(true) +{ + xml->get_widget("load_plugin_plugins_treeview", _plugins_treeview); + xml->get_widget("load_plugin_polyphonic_checkbutton", _polyphonic_checkbutton); + xml->get_widget("load_plugin_name_entry", _name_entry); + xml->get_widget("load_plugin_add_button", _add_button); + xml->get_widget("load_plugin_close_button", _close_button); + + xml->get_widget("load_plugin_filter_combo", _filter_combo); + xml->get_widget("load_plugin_search_entry", _search_entry); + + // Set up the plugins list + _plugins_liststore = Gtk::ListStore::create(_plugins_columns); + _plugins_treeview->set_model(_plugins_liststore); + _plugins_treeview->append_column("_Name", _plugins_columns._col_name); + _plugins_treeview->append_column("_Type", _plugins_columns._col_type); + _plugins_treeview->append_column("_Project", _plugins_columns._col_project); + _plugins_treeview->append_column("_Author", _plugins_columns._col_author); + _plugins_treeview->append_column("_URI", _plugins_columns._col_uri); + + // This could be nicer.. store the TreeViewColumns locally maybe? + _plugins_treeview->get_column(0)->set_sort_column(_plugins_columns._col_name); + _plugins_treeview->get_column(1)->set_sort_column(_plugins_columns._col_type); + _plugins_treeview->get_column(2)->set_sort_column(_plugins_columns._col_project); + _plugins_treeview->get_column(2)->set_sort_column(_plugins_columns._col_author); + _plugins_treeview->get_column(3)->set_sort_column(_plugins_columns._col_uri); + for (int i = 0; i < 5; ++i) { + _plugins_treeview->get_column(i)->set_resizable(true); + } + + // Set up the search criteria combobox + _criteria_liststore = Gtk::ListStore::create(_criteria_columns); + _filter_combo->set_model(_criteria_liststore); + + Gtk::TreeModel::iterator iter = _criteria_liststore->append(); + Gtk::TreeModel::Row row = *iter; + row[_criteria_columns._col_label] = "Name contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::NAME; + _filter_combo->set_active(iter); + + row = *(iter = _criteria_liststore->append()); + row[_criteria_columns._col_label] = "Type contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::TYPE; + + row = *(iter = _criteria_liststore->append()); + row[_criteria_columns._col_label] = "Project contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::PROJECT; + + row = *(iter = _criteria_liststore->append()); + row[_criteria_columns._col_label] = "Author contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::AUTHOR; + + row = *(iter = _criteria_liststore->append()); + row[_criteria_columns._col_label] = "URI contains"; + row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::URI; + _filter_combo->pack_start(_criteria_columns._col_label); + + _add_button->signal_clicked().connect( + sigc::mem_fun(this, &LoadPluginWindow::add_clicked)); + _close_button->signal_clicked().connect( + sigc::mem_fun(this, &Window::hide)); + _plugins_treeview->signal_row_activated().connect( + sigc::mem_fun(this, &LoadPluginWindow::plugin_activated)); + _search_entry->signal_activate().connect( + sigc::mem_fun(this, &LoadPluginWindow::add_clicked)); + _search_entry->signal_changed().connect( + sigc::mem_fun(this, &LoadPluginWindow::filter_changed)); + _name_entry->signal_changed().connect( + sigc::mem_fun(this, &LoadPluginWindow::name_changed)); + +#ifdef HAVE_NEW_GTKMM + _search_entry->signal_icon_release().connect( + sigc::mem_fun(this, &LoadPluginWindow::name_cleared)); +#endif + + _selection = _plugins_treeview->get_selection(); + _selection->set_mode(Gtk::SELECTION_MULTIPLE); + _selection->signal_changed().connect( + sigc::mem_fun(this, &LoadPluginWindow::plugin_selection_changed)); + + //m_add_button->grab_default(); +} + +void +LoadPluginWindow::present(SPtr<const GraphModel> graph, + Properties data) +{ + set_graph(graph); + _initial_data = data; + Gtk::Window::present(); +} + +/** Called every time the user types into the name input box. + * Used to display warning messages, and enable/disable the OK button. + */ +void +LoadPluginWindow::name_changed() +{ + // Toggle add button sensitivity according name legality + if (_selection->get_selected_rows().size() == 1) { + const string sym = _name_entry->get_text(); + if (!Raul::Symbol::is_valid(sym)) { + _add_button->property_sensitive() = false; + } else if (_app->store()->find(_graph->path().child(Raul::Symbol(sym))) + != _app->store()->end()) { + _add_button->property_sensitive() = false; + } else { + _add_button->property_sensitive() = true; + } + } +} + +#ifdef HAVE_NEW_GTKMM +void +LoadPluginWindow::name_cleared(Gtk::EntryIconPosition pos, const GdkEventButton* event) +{ + _search_entry->set_text(""); +} +#endif // HAVE_NEW_GTKMM + +/** Sets the graph controller for this window and initializes everything. + * + * This function MUST be called before using the window in any way! + */ +void +LoadPluginWindow::set_graph(SPtr<const GraphModel> graph) +{ + if (_graph) { + _graph = graph; + plugin_selection_changed(); + } else { + _graph = graph; + } +} + +/** Populates the plugin list on the first show. + * + * This is done here instead of construction time as the list population is + * really expensive and bogs down creation of a graph. This is especially + * important when many graph notifications are sent at one time from the + * engine. + */ +void +LoadPluginWindow::on_show() +{ + if (!_has_shown) { + _app->store()->signal_new_plugin().connect( + sigc::mem_fun(this, &LoadPluginWindow::add_plugin)); + _has_shown = true; + } + + if (_refresh_list) { + set_plugins(_app->store()->plugins()); + _refresh_list = false; + } + + Gtk::Window::on_show(); +} + +void +LoadPluginWindow::set_plugins(SPtr<const ClientStore::Plugins> plugins) +{ + _rows.clear(); + _plugins_liststore->clear(); + + for (const auto& p : *plugins.get()) { + add_plugin(p.second); + } + + _plugins_liststore->set_sort_column(1, Gtk::SORT_ASCENDING); + _plugins_treeview->columns_autosize(); +} + +void +LoadPluginWindow::new_plugin(SPtr<const PluginModel> pm) +{ + if (is_visible()) { + add_plugin(pm); + } else { + _refresh_list = true; + } +} + +static std::string +get_project_name(SPtr<const PluginModel> plugin) +{ + std::string name; + if (plugin->lilv_plugin()) { + LilvNode* project = lilv_plugin_get_project(plugin->lilv_plugin()); + if (!project) { + return ""; + } + + LilvNode* doap_name = lilv_new_uri( + plugin->lilv_world(), "http://usefulinc.com/ns/doap#name"); + LilvNodes* names = lilv_world_find_nodes( + plugin->lilv_world(), project, doap_name, nullptr); + + if (names) { + name = lilv_node_as_string(lilv_nodes_get_first(names)); + } + + lilv_nodes_free(names); + lilv_node_free(doap_name); + lilv_node_free(project); + } + return name; +} + +static std::string +get_author_name(SPtr<const PluginModel> plugin) +{ + std::string name; + if (plugin->lilv_plugin()) { + LilvNode* author = lilv_plugin_get_author_name(plugin->lilv_plugin()); + if (author) { + name = lilv_node_as_string(author); + } + lilv_node_free(author); + } + return name; +} + +void +LoadPluginWindow::set_row(Gtk::TreeModel::Row& row, + SPtr<const PluginModel> plugin) +{ + const URIs& uris = _app->uris(); + const Atom& name = plugin->get_property(uris.doap_name); + if (name.is_valid() && name.type() == uris.forge.String) { + row[_plugins_columns._col_name] = name.ptr<char>(); + } + + if (uris.lv2_Plugin == plugin->type()) { + row[_plugins_columns._col_type] = lilv_node_as_string( + lilv_plugin_class_get_label( + lilv_plugin_get_class(plugin->lilv_plugin()))); + + row[_plugins_columns._col_project] = get_project_name(plugin); + row[_plugins_columns._col_author] = get_author_name(plugin); + } else if (uris.ingen_Internal == plugin->type()) { + row[_plugins_columns._col_type] = "Internal"; + row[_plugins_columns._col_project] = "Ingen"; + row[_plugins_columns._col_author] = "David Robillard"; + } else if (uris.ingen_Graph == plugin->type()) { + row[_plugins_columns._col_type] = "Graph"; + } else { + row[_plugins_columns._col_type] = ""; + } + + row[_plugins_columns._col_uri] = plugin->uri().string(); + row[_plugins_columns._col_plugin] = plugin; +} + +void +LoadPluginWindow::add_plugin(SPtr<const PluginModel> plugin) +{ + if (plugin->lilv_plugin() && lilv_plugin_is_replaced(plugin->lilv_plugin())) { + return; + } + + Gtk::TreeModel::iterator iter = _plugins_liststore->append(); + Gtk::TreeModel::Row row = *iter; + _rows.emplace(plugin->uri(), iter); + + set_row(row, plugin); + + plugin->signal_property().connect( + sigc::bind<0>(sigc::mem_fun(this, &LoadPluginWindow::plugin_property_changed), + plugin->uri())); +} + +///// Event Handlers ////// + +void +LoadPluginWindow::plugin_activated(const Gtk::TreeModel::Path& path, + Gtk::TreeViewColumn* col) +{ + add_clicked(); +} + +void +LoadPluginWindow::plugin_selection_changed() +{ + size_t n_selected = _selection->get_selected_rows().size(); + if (n_selected == 0) { + _name_offset = 0; + _name_entry->set_text(""); + _name_entry->set_sensitive(false); + } else if (n_selected == 1) { + Gtk::TreeModel::iterator iter = _plugins_liststore->get_iter( + *_selection->get_selected_rows().begin()); + if (iter) { + Gtk::TreeModel::Row row = *iter; + SPtr<const PluginModel> p = row.get_value( + _plugins_columns._col_plugin); + _name_offset = _app->store()->child_name_offset( + _graph->path(), p->default_block_symbol()); + _name_entry->set_text(generate_module_name(p, _name_offset)); + _name_entry->set_sensitive(true); + } else { + _name_offset = 0; + _name_entry->set_text(""); + _name_entry->set_sensitive(false); + } + } else { + _name_entry->set_text(""); + _name_entry->set_sensitive(false); + } +} + +/** Generate an automatic name for this Node. + * + * Offset is an offset of the number that will be appended to the plugin's + * label, needed if the user adds multiple plugins faster than the engine + * sends the notification back. + */ +string +LoadPluginWindow::generate_module_name(SPtr<const PluginModel> plugin, + int offset) +{ + std::stringstream ss; + ss << plugin->default_block_symbol(); + if (offset != 0) { + ss << "_" << offset; + } + return ss.str(); +} + +void +LoadPluginWindow::load_plugin(const Gtk::TreeModel::iterator& iter) +{ + const URIs& uris = _app->uris(); + Gtk::TreeModel::Row row = *iter; + SPtr<const PluginModel> plugin = row.get_value(_plugins_columns._col_plugin); + bool polyphonic = _polyphonic_checkbutton->get_active(); + string name = _name_entry->get_text(); + + if (name.empty()) { + name = generate_module_name(plugin, _name_offset); + } + + if (name.empty() || !Raul::Symbol::is_valid(name)) { + Gtk::MessageDialog dialog( + *this, + "Unable to choose a default name, please provide one", + false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + + dialog.run(); + } else { + Raul::Path path = _graph->path().child(Raul::Symbol::symbolify(name)); + Properties props = _initial_data; + props.emplace(uris.rdf_type, Property(uris.ingen_Block)); + props.emplace(uris.lv2_prototype, _app->forge().make_urid(plugin->uri())); + props.emplace(uris.ingen_polyphonic, _app->forge().make(polyphonic)); + _app->interface()->put(path_to_uri(path), props); + + if (_selection->get_selected_rows().size() == 1) { + _name_offset = (_name_offset == 0) ? 2 : _name_offset + 1; + _name_entry->set_text(generate_module_name(plugin, _name_offset)); + } + + // Cascade next block + Atom& x = _initial_data.find(uris.ingen_canvasX)->second; + x = _app->forge().make(x.get<float>() + 20.0f); + Atom& y = _initial_data.find(uris.ingen_canvasY)->second; + y = _app->forge().make(y.get<float>() + 20.0f); + } +} + +void +LoadPluginWindow::add_clicked() +{ + _selection->selected_foreach_iter( + sigc::mem_fun(*this, &LoadPluginWindow::load_plugin)); +} + +void +LoadPluginWindow::filter_changed() +{ + _rows.clear(); + _plugins_liststore->clear(); + string search = _search_entry->get_text(); + transform(search.begin(), search.end(), search.begin(), ::toupper); + + // Get selected criteria + const Gtk::TreeModel::Row row = *(_filter_combo->get_active()); + CriteriaColumns::Criteria criteria = row[_criteria_columns._col_criteria]; + + string field; + + Gtk::TreeModel::Row model_row; + Gtk::TreeModel::iterator model_iter; + size_t num_visible = 0; + const URIs& uris = _app->uris(); + + for (const auto& p : *_app->store()->plugins().get()) { + const SPtr<PluginModel> plugin = p.second; + const Atom& name = plugin->get_property(uris.doap_name); + + switch (criteria) { + case CriteriaColumns::Criteria::NAME: + if (name.is_valid() && name.type() == uris.forge.String) { + field = name.ptr<char>(); + } + break; + case CriteriaColumns::Criteria::TYPE: + if (plugin->lilv_plugin()) { + field = lilv_node_as_string( + lilv_plugin_class_get_label( + lilv_plugin_get_class(plugin->lilv_plugin()))); + } + break; + case CriteriaColumns::Criteria::PROJECT: + field = get_project_name(plugin); + break; + case CriteriaColumns::Criteria::AUTHOR: + field = get_author_name(plugin); + break; + case CriteriaColumns::Criteria::URI: + field = plugin->uri(); + break; + } + + transform(field.begin(), field.end(), field.begin(), ::toupper); + + if (field.find(search) != string::npos) { + model_iter = _plugins_liststore->append(); + model_row = *model_iter; + set_row(model_row, plugin); + ++num_visible; + } + } + + if (num_visible == 1) { + _selection->unselect_all(); + _selection->select(model_iter); + } +} + +bool +LoadPluginWindow::on_key_press_event(GdkEventKey* event) +{ + if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) { + hide(); + return true; + } else { + return Gtk::Window::on_key_press_event(event); + } +} + +void +LoadPluginWindow::plugin_property_changed(const URI& plugin, + const URI& predicate, + const Atom& value) +{ + const URIs& uris = _app->uris(); + if (predicate == uris.doap_name) { + Rows::const_iterator i = _rows.find(plugin); + if (i != _rows.end() && value.type() == uris.forge.String) { + (*i->second)[_plugins_columns._col_name] = value.ptr<char>(); + } + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/LoadPluginWindow.hpp b/src/gui/LoadPluginWindow.hpp new file mode 100644 index 00000000..3874b8dd --- /dev/null +++ b/src/gui/LoadPluginWindow.hpp @@ -0,0 +1,162 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_LOADPLUGINWINDOW_HPP +#define INGEN_GUI_LOADPLUGINWINDOW_HPP + +#include <map> +#include <string> + +#include <gtkmm/builder.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/treemodel.h> +#include <gtkmm/treeview.h> + +#include "ingen/Node.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/types.hpp" +#include "ingen_config.h" + +#include "Window.hpp" + +namespace Ingen { + +namespace Client { +class GraphModel; +class PluginModel; +} + +namespace GUI { + +/** 'Load Plugin' window. + * + * Loaded from XML as a derived object. + * + * \ingroup GUI + */ +class LoadPluginWindow : public Window +{ +public: + LoadPluginWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void set_graph(SPtr<const Client::GraphModel> graph); + void set_plugins(SPtr<const Client::ClientStore::Plugins> plugins); + + void add_plugin(SPtr<const Client::PluginModel> plugin); + + void present(SPtr<const Client::GraphModel> graph, + Properties data); + +protected: + void on_show(); + bool on_key_press_event(GdkEventKey* event); + +private: + /** Columns for the plugin list */ + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns() { + add(_col_name); + add(_col_type); + add(_col_project); + add(_col_author); + add(_col_uri); + add(_col_plugin); + } + + Gtk::TreeModelColumn<Glib::ustring> _col_name; + Gtk::TreeModelColumn<Glib::ustring> _col_type; + Gtk::TreeModelColumn<Glib::ustring> _col_project; + Gtk::TreeModelColumn<Glib::ustring> _col_author; + Gtk::TreeModelColumn<Glib::ustring> _col_uri; + + // Not displayed: + Gtk::TreeModelColumn< SPtr<const Client::PluginModel> > _col_plugin; + }; + + /** Column for the filter criteria combo box. */ + class CriteriaColumns : public Gtk::TreeModel::ColumnRecord { + public: + enum class Criteria { NAME, TYPE, PROJECT, AUTHOR, URI, }; + + CriteriaColumns() { + add(_col_label); + add(_col_criteria); + } + + Gtk::TreeModelColumn<Glib::ustring> _col_label; + Gtk::TreeModelColumn<Criteria> _col_criteria; + }; + + void add_clicked(); + void filter_changed(); + void clear_clicked(); + void name_changed(); +#ifdef HAVE_NEW_GTKMM + void name_cleared(Gtk::EntryIconPosition pos, const GdkEventButton* event); +#endif + + void set_row(Gtk::TreeModel::Row& row, + SPtr<const Client::PluginModel> plugin); + + void new_plugin(SPtr<const Client::PluginModel> pm); + + void plugin_property_changed(const URI& plugin, + const URI& predicate, + const Atom& value); + + void plugin_activated(const Gtk::TreeModel::Path& path, Gtk::TreeViewColumn* col); + void plugin_selection_changed(); + + std::string generate_module_name(SPtr<const Client::PluginModel> plugin, + int offset=0); + + void load_plugin(const Gtk::TreeModel::iterator& iter); + + Properties _initial_data; + + SPtr<const Client::GraphModel> _graph; + + typedef std::map<URI, Gtk::TreeModel::iterator> Rows; + Rows _rows; + + Glib::RefPtr<Gtk::ListStore> _plugins_liststore; + ModelColumns _plugins_columns; + + Glib::RefPtr<Gtk::ListStore> _criteria_liststore; + CriteriaColumns _criteria_columns; + + Glib::RefPtr<Gtk::TreeSelection> _selection; + + int _name_offset; // see comments for generate_plugin_name + + bool _has_shown; + bool _refresh_list; + Gtk::TreeView* _plugins_treeview; + Gtk::CheckButton* _polyphonic_checkbutton; + Gtk::Entry* _name_entry; + Gtk::Button* _close_button; + Gtk::Button* _add_button; + Gtk::ComboBox* _filter_combo; + Gtk::Entry* _search_entry; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_LOADPLUGINWINDOW_HPP diff --git a/src/gui/MessagesWindow.cpp b/src/gui/MessagesWindow.cpp new file mode 100644 index 00000000..581e732c --- /dev/null +++ b/src/gui/MessagesWindow.cpp @@ -0,0 +1,141 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/URIs.hpp" + +#include "App.hpp" +#include "MessagesWindow.hpp" + +namespace Ingen { +namespace GUI { +using std::string; + +MessagesWindow::MessagesWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) +{ + xml->get_widget("messages_textview", _textview); + xml->get_widget("messages_clear_button", _clear_button); + xml->get_widget("messages_close_button", _close_button); + + _clear_button->signal_clicked().connect(sigc::mem_fun(this, &MessagesWindow::clear_clicked)); + _close_button->signal_clicked().connect(sigc::mem_fun(this, &Window::hide)); + + for (int s = Gtk::STATE_NORMAL; s <= Gtk::STATE_INSENSITIVE; ++s) { + _textview->modify_base((Gtk::StateType)s, Gdk::Color("#000000")); + _textview->modify_text((Gtk::StateType)s, Gdk::Color("#EEEEEC")); + } +} + +void +MessagesWindow::init_window(App& app) +{ + Glib::RefPtr<Gtk::TextTag> tag = Gtk::TextTag::create(); + tag->property_foreground() = "#EF2929"; + _tags.emplace(app.uris().log_Error, tag); + _error_tag = tag; + + tag = Gtk::TextTag::create(); + tag->property_foreground() = "#FCAF3E"; + _tags.emplace(app.uris().log_Warning, tag); + + tag = Gtk::TextTag::create(); + tag->property_foreground() = "#8AE234"; + _tags.emplace(app.uris().log_Trace, tag); + + for (const auto& t : _tags) { + _textview->get_buffer()->get_tag_table()->add(t.second); + } +} + +void +MessagesWindow::post_error(const string& msg) +{ + Glib::RefPtr<Gtk::TextBuffer> text_buf = _textview->get_buffer(); + text_buf->insert_with_tag(text_buf->end(), msg, _error_tag); + text_buf->insert(text_buf->end(), "\n"); + + if (!_clear_button->is_sensitive()) { + _clear_button->set_sensitive(true); + } + + set_urgency_hint(true); + if (!is_visible()) { + present(); + } +} + +int +MessagesWindow::log(LV2_URID type, const char* fmt, va_list args) +{ + std::lock_guard<std::mutex> lock(_mutex); + +#ifdef HAVE_VASPRINTF + char* buf = nullptr; + const int len = vasprintf(&buf, fmt, args); +#else + char* buf = g_strdup_vprintf(fmt, args); + const int len = strlen(buf); +#endif + + _stream << type << ' ' << buf << '\0'; + free(buf); + + return len; +} + +void +MessagesWindow::flush() +{ + while (true) { + LV2_URID type; + std::string line; + { + std::lock_guard<std::mutex> lock(_mutex); + if (!_stream.rdbuf()->in_avail()) { + return; + } + _stream >> type; + std::getline(_stream, line, '\0'); + } + + Glib::RefPtr<Gtk::TextBuffer> text_buf = _textview->get_buffer(); + + auto t = _tags.find(type); + if (t != _tags.end()) { + text_buf->insert_with_tag(text_buf->end(), line, t->second); + } else { + text_buf->insert(text_buf->end(), line); + } + } + + if (!_clear_button->is_sensitive()) { + _clear_button->set_sensitive(true); + } +} + +void +MessagesWindow::clear_clicked() +{ + Glib::RefPtr<Gtk::TextBuffer> text_buf = _textview->get_buffer(); + text_buf->erase(text_buf->begin(), text_buf->end()); + _clear_button->set_sensitive(false); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/MessagesWindow.hpp b/src/gui/MessagesWindow.hpp new file mode 100644 index 00000000..fa9eae1d --- /dev/null +++ b/src/gui/MessagesWindow.hpp @@ -0,0 +1,70 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_MESSAGESWINDOW_HPP +#define INGEN_GUI_MESSAGESWINDOW_HPP + +#include <mutex> +#include <sstream> +#include <string> + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/textview.h> +#include "lv2/lv2plug.in/ns/ext/log/log.h" + +#include "Window.hpp" + +namespace Ingen { +namespace GUI { + +/** Messages Window. + * + * Loaded from XML as a derived object. + * This is shown when errors occur (e.g. during graph loading). + * + * \ingroup GUI + */ +class MessagesWindow : public Window +{ +public: + MessagesWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init_window(App& app); + + int log(LV2_URID type, const char* fmt, va_list args); + void flush(); + + void post_error(const std::string& msg); + +private: + void clear_clicked(); + + std::mutex _mutex; + std::stringstream _stream; + Gtk::TextView* _textview; + Gtk::Button* _clear_button; + Gtk::Button* _close_button; + + Glib::RefPtr<Gtk::TextTag> _error_tag; + std::map< LV2_URID, Glib::RefPtr<Gtk::TextTag> > _tags; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_MESSAGESWINDOW_HPP diff --git a/src/gui/NewSubgraphWindow.cpp b/src/gui/NewSubgraphWindow.cpp new file mode 100644 index 00000000..f9dc8fc4 --- /dev/null +++ b/src/gui/NewSubgraphWindow.cpp @@ -0,0 +1,119 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/Interface.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "NewSubgraphWindow.hpp" +#include "GraphView.hpp" + +namespace Ingen { +namespace GUI { + +NewSubgraphWindow::NewSubgraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) +{ + xml->get_widget("new_subgraph_name_entry", _name_entry); + xml->get_widget("new_subgraph_message_label", _message_label); + xml->get_widget("new_subgraph_polyphony_spinbutton", _poly_spinbutton); + xml->get_widget("new_subgraph_ok_button", _ok_button); + xml->get_widget("new_subgraph_cancel_button", _cancel_button); + + _name_entry->signal_changed().connect(sigc::mem_fun(this, &NewSubgraphWindow::name_changed)); + _ok_button->signal_clicked().connect(sigc::mem_fun(this, &NewSubgraphWindow::ok_clicked)); + _cancel_button->signal_clicked().connect(sigc::mem_fun(this, &NewSubgraphWindow::cancel_clicked)); + + _ok_button->property_sensitive() = false; + + _poly_spinbutton->get_adjustment()->configure(1.0, 1.0, 128, 1.0, 10.0, 0); +} + +void +NewSubgraphWindow::present(SPtr<const Client::GraphModel> graph, + Properties data) +{ + set_graph(graph); + _initial_data = data; + Gtk::Window::present(); +} + +/** Sets the graph controller for this window and initializes everything. + * + * This function MUST be called before using the window in any way! + */ +void +NewSubgraphWindow::set_graph(SPtr<const Client::GraphModel> graph) +{ + _graph = graph; +} + +/** Called every time the user types into the name input box. + * Used to display warning messages, and enable/disable the OK button. + */ +void +NewSubgraphWindow::name_changed() +{ + std::string name = _name_entry->get_text(); + if (!Raul::Symbol::is_valid(name)) { + _message_label->set_text("Name contains invalid characters."); + _ok_button->property_sensitive() = false; + } else if (_app->store()->find(_graph->path().child(Raul::Symbol(name))) + != _app->store()->end()) { + _message_label->set_text("An object already exists with that name."); + _ok_button->property_sensitive() = false; + } else { + _message_label->set_text(""); + _ok_button->property_sensitive() = true; + } +} + +void +NewSubgraphWindow::ok_clicked() +{ + const uint32_t poly = _poly_spinbutton->get_value_as_int(); + const Raul::Path path = _graph->path().child( + Raul::Symbol::symbolify(_name_entry->get_text())); + + // Create graph + Properties props; + props.emplace(_app->uris().rdf_type, Property(_app->uris().ingen_Graph)); + props.emplace(_app->uris().ingen_polyphony, _app->forge().make(int32_t(poly))); + props.emplace(_app->uris().ingen_enabled, _app->forge().make(bool(true))); + _app->interface()->put( + path_to_uri(path), props, Resource::Graph::INTERNAL); + + // Set external (block perspective) properties + props = _initial_data; + props.emplace(_app->uris().rdf_type, Property(_app->uris().ingen_Graph)); + _app->interface()->put( + path_to_uri(path), _initial_data, Resource::Graph::EXTERNAL); + + hide(); +} + +void +NewSubgraphWindow::cancel_clicked() +{ + hide(); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/NewSubgraphWindow.hpp b/src/gui/NewSubgraphWindow.hpp new file mode 100644 index 00000000..395856ba --- /dev/null +++ b/src/gui/NewSubgraphWindow.hpp @@ -0,0 +1,72 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_NEWSUBGRAPHWINDOW_HPP +#define INGEN_GUI_NEWSUBGRAPHWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/label.h> +#include <gtkmm/spinbutton.h> + +#include "ingen/Node.hpp" +#include "ingen/types.hpp" + +#include "Window.hpp" + +namespace Ingen { + +namespace Client { class GraphModel; } + +namespace GUI { + +/** 'New Subgraph' window. + * + * Loaded from XML as a derived object. + * + * \ingroup GUI + */ +class NewSubgraphWindow : public Window +{ +public: + NewSubgraphWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void set_graph(SPtr<const Client::GraphModel> graph); + + void present(SPtr<const Client::GraphModel> graph, + Properties data); + +private: + void name_changed(); + void ok_clicked(); + void cancel_clicked(); + + Properties _initial_data; + SPtr<const Client::GraphModel> _graph; + + Gtk::Entry* _name_entry; + Gtk::Label* _message_label; + Gtk::SpinButton* _poly_spinbutton; + Gtk::Button* _ok_button; + Gtk::Button* _cancel_button; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_NEWSUBGRAPHWINDOW_HPP diff --git a/src/gui/NodeMenu.cpp b/src/gui/NodeMenu.cpp new file mode 100644 index 00000000..1b1b1677 --- /dev/null +++ b/src/gui/NodeMenu.cpp @@ -0,0 +1,253 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include <gtkmm/entry.h> +#include <gtkmm/filechooserdialog.h> +#include <gtkmm/image.h> +#include <gtkmm/stock.h> + +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "lv2/lv2plug.in/ns/ext/presets/presets.h" + +#include "App.hpp" +#include "NodeMenu.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +NodeMenu::NodeMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : ObjectMenu(cobject, xml) + , _presets_menu(nullptr) +{ + xml->get_widget("node_popup_gui_menuitem", _popup_gui_menuitem); + xml->get_widget("node_embed_gui_menuitem", _embed_gui_menuitem); + xml->get_widget("node_enabled_menuitem", _enabled_menuitem); + xml->get_widget("node_randomize_menuitem", _randomize_menuitem); +} + +void +NodeMenu::init(App& app, SPtr<const Client::BlockModel> block) +{ + ObjectMenu::init(app, block); + + _learn_menuitem->signal_activate().connect( + sigc::mem_fun(this, &NodeMenu::on_menu_learn)); + _popup_gui_menuitem->signal_activate().connect( + sigc::mem_fun(signal_popup_gui, &sigc::signal<void>::emit)); + _embed_gui_menuitem->signal_toggled().connect( + sigc::mem_fun(this, &NodeMenu::on_menu_embed_gui)); + _enabled_menuitem->signal_toggled().connect( + sigc::mem_fun(this, &NodeMenu::on_menu_enabled)); + _randomize_menuitem->signal_activate().connect( + sigc::mem_fun(this, &NodeMenu::on_menu_randomize)); + + SPtr<PluginModel> plugin = block->plugin_model(); + if (plugin) { + // Get the plugin to receive related presets + _preset_connection = plugin->signal_preset().connect( + sigc::mem_fun(this, &NodeMenu::add_preset)); + + if (!plugin->fetched()) { + _app->interface()->get(plugin->uri()); + plugin->set_fetched(true); + } + } + + if (plugin && plugin->has_ui()) { + _popup_gui_menuitem->show(); + _embed_gui_menuitem->show(); + const Atom& ui_embedded = block->get_property( + _app->uris().ingen_uiEmbedded); + _embed_gui_menuitem->set_active( + ui_embedded.is_valid() && ui_embedded.get<int32_t>()); + } else { + _popup_gui_menuitem->hide(); + _embed_gui_menuitem->hide(); + } + + const Atom& enabled = block->get_property(_app->uris().ingen_enabled); + _enabled_menuitem->set_active(!enabled.is_valid() || enabled.get<int32_t>()); + + if (plugin && _app->uris().lv2_Plugin == plugin->type()) { + _presets_menu = Gtk::manage(new Gtk::Menu()); + _presets_menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + "_Save Preset...", + sigc::mem_fun(this, &NodeMenu::on_save_preset_activated))); + _presets_menu->items().push_back(Gtk::Menu_Helpers::SeparatorElem()); + + for (const auto& p : plugin->presets()) { + add_preset(p.first, p.second); + } + + items().push_front( + Gtk::Menu_Helpers::ImageMenuElem( + "_Presets", + *(manage(new Gtk::Image(Gtk::Stock::INDEX, Gtk::ICON_SIZE_MENU))))); + + Gtk::MenuItem* presets_menu_item = &(items().front()); + presets_menu_item->set_submenu(*_presets_menu); + } + + if (has_control_inputs()) { + _randomize_menuitem->show(); + } else { + _randomize_menuitem->hide(); + } + + if (plugin && (plugin->uri() == "http://drobilla.net/ns/ingen-internals#Controller" + || plugin->uri() == "http://drobilla.net/ns/ingen-internals#Trigger")) { + _learn_menuitem->show(); + } else { + _learn_menuitem->hide(); + } + + if (!_popup_gui_menuitem->is_visible() && + !_embed_gui_menuitem->is_visible() && + !_randomize_menuitem->is_visible()) { + _separator_menuitem->hide(); + } + + _enable_signal = true; +} + +void +NodeMenu::add_preset(const URI& uri, const std::string& label) +{ + if (_presets_menu) { + _presets_menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + label, + sigc::bind(sigc::mem_fun(this, &NodeMenu::on_preset_activated), + uri))); + } +} + +void +NodeMenu::on_menu_embed_gui() +{ + signal_embed_gui.emit(_embed_gui_menuitem->get_active()); +} + +void +NodeMenu::on_menu_enabled() +{ + _app->set_property(_object->uri(), + _app->uris().ingen_enabled, + _app->forge().make(bool(_enabled_menuitem->get_active()))); +} + +void +NodeMenu::on_menu_randomize() +{ + _app->interface()->bundle_begin(); + + const SPtr<const BlockModel> bm = block(); + for (const auto& p : bm->ports()) { + if (p->is_input() && _app->can_control(p.get())) { + float min = 0.0f, max = 1.0f; + bm->port_value_range(p, min, max, _app->sample_rate()); + const float val = g_random_double_range(0.0, 1.0) * (max - min) + min; + _app->set_property(p->uri(), + _app->uris().ingen_value, + _app->forge().make(val)); + } + } + + _app->interface()->bundle_end(); +} + +void +NodeMenu::on_menu_disconnect() +{ + _app->interface()->disconnect_all(_object->parent()->path(), _object->path()); +} + +void +NodeMenu::on_save_preset_activated() +{ + Gtk::FileChooserDialog dialog("Save Preset", Gtk::FILE_CHOOSER_ACTION_SAVE); + dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL); + dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK); + dialog.set_default_response(Gtk::RESPONSE_OK); + dialog.set_current_folder(Glib::build_filename(Glib::get_home_dir(), ".lv2")); + + Gtk::HBox* extra = Gtk::manage(new Gtk::HBox()); + Gtk::Label* label = Gtk::manage(new Gtk::Label("URI (Optional): ")); + Gtk::Entry* entry = Gtk::manage(new Gtk::Entry()); + extra->pack_start(*label, false, true, 4); + extra->pack_start(*entry, true, true, 4); + extra->show_all(); + dialog.set_extra_widget(*Gtk::manage(extra)); + + if (dialog.run() == Gtk::RESPONSE_OK) { + const std::string user_uri = dialog.get_uri(); + const std::string user_path = Glib::filename_from_uri(user_uri); + const std::string dirname = Glib::path_get_dirname(user_path); + const std::string basename = Glib::path_get_basename(user_path); + const std::string sym = Raul::Symbol::symbolify(basename); + const std::string plugname = block()->plugin_model()->human_name(); + const std::string prefix = Raul::Symbol::symbolify(plugname); + const std::string bundle = prefix + "_" + sym + ".preset.lv2/"; + const std::string file = sym + ".ttl"; + const std::string real_path = Glib::build_filename(dirname, bundle, file); + const std::string real_uri = Glib::filename_to_uri(real_path); + + Properties props{ + { _app->uris().rdf_type, + _app->uris().pset_Preset }, + { _app->uris().rdfs_label, + _app->forge().alloc(basename) }, + { _app->uris().lv2_prototype, + _app->forge().make_urid(block()->uri()) }}; + _app->interface()->put(URI(real_uri), props); + } +} + +void +NodeMenu::on_preset_activated(const std::string& uri) +{ + _app->set_property(block()->uri(), + _app->uris().pset_preset, + _app->forge().make_urid(URI(uri))); +} + +bool +NodeMenu::has_control_inputs() +{ + for (const auto& p : block()->ports()) { + if (p->is_input() && p->is_numeric()) { + return true; + } + } + + return false; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/NodeMenu.hpp b/src/gui/NodeMenu.hpp new file mode 100644 index 00000000..5d9f1e6d --- /dev/null +++ b/src/gui/NodeMenu.hpp @@ -0,0 +1,75 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_NODEMENU_HPP +#define INGEN_GUI_NODEMENU_HPP + +#include <string> + +#include <gtkmm/builder.h> +#include <gtkmm/menu.h> +#include <gtkmm/menushell.h> + +#include "ObjectMenu.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/types.hpp" + +namespace Ingen { +namespace GUI { + +/** Menu for a Node. + * + * \ingroup GUI + */ +class NodeMenu : public ObjectMenu +{ +public: + NodeMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app, SPtr<const Client::BlockModel> block); + + bool has_control_inputs(); + + sigc::signal<void> signal_popup_gui; + sigc::signal<void, bool> signal_embed_gui; + +protected: + SPtr<const Client::BlockModel> block() const { + return dynamic_ptr_cast<const Client::BlockModel>(_object); + } + + void add_preset(const URI& uri, const std::string& label); + + void on_menu_disconnect(); + void on_menu_embed_gui(); + void on_menu_enabled(); + void on_menu_randomize(); + void on_save_preset_activated(); + void on_preset_activated(const std::string& uri); + + Gtk::MenuItem* _popup_gui_menuitem; + Gtk::CheckMenuItem* _embed_gui_menuitem; + Gtk::CheckMenuItem* _enabled_menuitem; + Gtk::MenuItem* _randomize_menuitem; + Gtk::Menu* _presets_menu; + sigc::connection _preset_connection; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_NODEMENU_HPP diff --git a/src/gui/NodeModule.cpp b/src/gui/NodeModule.cpp new file mode 100644 index 00000000..dadffff0 --- /dev/null +++ b/src/gui/NodeModule.cpp @@ -0,0 +1,518 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> + +#include <gtkmm/eventbox.h> + +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "ingen/Atom.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/client/PluginUI.hpp" + +#include "App.hpp" +#include "GraphCanvas.hpp" +#include "GraphWindow.hpp" +#include "NodeMenu.hpp" +#include "NodeModule.hpp" +#include "Port.hpp" +#include "RenameWindow.hpp" +#include "Style.hpp" +#include "SubgraphModule.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" +#include "ingen_config.h" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +NodeModule::NodeModule(GraphCanvas& canvas, + SPtr<const BlockModel> block) + : Ganv::Module(canvas, block->path().symbol(), 0, 0, true) + , _block(block) + , _gui_widget(nullptr) + , _gui_window(nullptr) + , _initialised(false) +{ + block->signal_new_port().connect( + sigc::mem_fun(this, &NodeModule::new_port_view)); + block->signal_removed_port().connect( + sigc::hide_return(sigc::mem_fun(this, &NodeModule::delete_port_view))); + block->signal_property().connect( + sigc::mem_fun(this, &NodeModule::property_changed)); + block->signal_moved().connect( + sigc::mem_fun(this, &NodeModule::rename)); + + signal_event().connect( + sigc::mem_fun(this, &NodeModule::on_event)); + + signal_moved().connect( + sigc::mem_fun(this, &NodeModule::store_location)); + + signal_selected().connect( + sigc::mem_fun(this, &NodeModule::on_selected)); + + const PluginModel* plugin = dynamic_cast<const PluginModel*>(block->plugin()); + if (plugin) { + plugin->signal_changed().connect( + sigc::mem_fun(this, &NodeModule::plugin_changed)); + } + + for (const auto& p : block->properties()) { + property_changed(p.first, p.second); + } + + if (_block->has_property(app().uris().ingen_uiEmbedded, + app().uris().forge.make(true))) { + // Schedule idle callback to embed GUI once ports arrive + Glib::signal_timeout().connect( + sigc::mem_fun(*this, &NodeModule::idle_init), 25, G_PRIORITY_DEFAULT_IDLE); + } else { + _initialised = true; + } +} + +NodeModule::~NodeModule() +{ + delete _gui_widget; + delete _gui_window; +} + +bool +NodeModule::idle_init() +{ + if (_block->ports().size() == 0) { + return true; // Need to embed GUI, but ports haven't shown up yet + } + + // Ports have arrived, embed GUI and deregister this callback + embed_gui(true); + _initialised = true; + return false; +} + +bool +NodeModule::show_menu(GdkEventButton* ev) +{ + WidgetFactory::get_widget_derived("object_menu", _menu); + if (!_menu) { + app().log().error("Failed to load object menu widget\n"); + return false; + } + + _menu->init(app(), _block); + _menu->signal_embed_gui.connect( + sigc::mem_fun(this, &NodeModule::on_embed_gui_toggled)); + _menu->signal_popup_gui.connect( + sigc::hide_return(sigc::mem_fun(this, &NodeModule::popup_gui))); + _menu->popup(ev->button, ev->time); + return true; +} + +NodeModule* +NodeModule::create(GraphCanvas& canvas, + SPtr<const BlockModel> block, + bool human) +{ + SPtr<const GraphModel> graph = dynamic_ptr_cast<const GraphModel>(block); + + NodeModule* ret = (graph) + ? new SubgraphModule(canvas, graph) + : new NodeModule(canvas, block); + + for (const auto& p : block->properties()) { + ret->property_changed(p.first, p.second); + } + + for (const auto& p : block->ports()) { + ret->new_port_view(p); + } + + ret->set_stacked(block->polyphonic()); + + if (human) { + ret->show_human_names(human); // FIXME: double port iteration + } + + return ret; +} + +App& +NodeModule::app() const +{ + return ((GraphCanvas*)canvas())->app(); +} + +void +NodeModule::show_human_names(bool b) +{ + const URIs& uris = app().uris(); + + if (b) { + set_label(block()->label().c_str()); + } else { + set_label(block()->symbol().c_str()); + } + + for (iterator i = begin(); i != end(); ++i) { + Ingen::GUI::Port* const port = dynamic_cast<Ingen::GUI::Port*>(*i); + Glib::ustring label(port->model()->symbol().c_str()); + if (b) { + const Atom& name_property = port->model()->get_property(uris.lv2_name); + if (name_property.type() == uris.forge.String) { + label = name_property.ptr<char>(); + } else { + Glib::ustring hn = block()->plugin_model()->port_human_name( + port->model()->index()); + if (!hn.empty()) { + label = hn; + } + } + } + (*i)->set_label(label.c_str()); + } +} + +void +NodeModule::port_activity(uint32_t index, const Atom& value) +{ + const URIs& uris = app().uris(); + if (!_plugin_ui) { + return; + } + + if (_block->get_port(index)->is_a(uris.atom_AtomPort)) { + _plugin_ui->port_event(index, + lv2_atom_total_size(value.atom()), + uris.atom_eventTransfer, + value.atom()); + } +} + +void +NodeModule::port_value_changed(uint32_t index, const Atom& value) +{ + const URIs& uris = app().uris(); + if (!_plugin_ui) { + return; + } + + if (value.type() == uris.atom_Float && + _block->get_port(index)->is_numeric()) { + _plugin_ui->port_event(index, sizeof(float), 0, value.ptr<float>()); + } else { + _plugin_ui->port_event(index, + lv2_atom_total_size(value.atom()), + uris.atom_eventTransfer, + value.atom()); + } +} + +void +NodeModule::plugin_changed() +{ + for (iterator p = begin(); p != end(); ++p) { + dynamic_cast<Ingen::GUI::Port*>(*p)->update_metadata(); + } +} + +void +NodeModule::on_embed_gui_toggled(bool embed) +{ + embed_gui(embed); + app().set_property(_block->uri(), + app().uris().ingen_uiEmbedded, + app().forge().make(embed)); +} + +void +NodeModule::embed_gui(bool embed) +{ + if (embed) { + if (_gui_window) { + app().log().warn("LV2 GUI already popped up, cannot embed\n"); + return; + } + + if (!_plugin_ui) { + _plugin_ui = _block->plugin_model()->ui(app().world(), _block); + } + + if (_plugin_ui) { + _plugin_ui->signal_property_changed().connect( + sigc::mem_fun(app(), &App::set_property)); + + if (!_plugin_ui->instantiate()) { + app().log().error("Failed to instantiate LV2 UI\n"); + } else { + GtkWidget* c_widget = (GtkWidget*)_plugin_ui->get_widget(); + _gui_widget = Glib::wrap(c_widget); + + Gtk::Container* container = new Gtk::EventBox(); + container->set_name("IngenEmbeddedUI"); + container->set_border_width(4.0); + container->add(*_gui_widget); + Ganv::Module::embed(container); + } + } else { + app().log().error("Failed to create LV2 UI\n"); + } + + if (_gui_widget) { + _gui_widget->show_all(); + set_control_values(); + } + + } else { // un-embed + Ganv::Module::embed(nullptr); + _plugin_ui.reset(); + } +} + +void +NodeModule::rename() +{ + if (app().world()->conf().option("port-labels").get<int32_t>() && + !app().world()->conf().option("human-names").get<int32_t>()) { + set_label(_block->path().symbol()); + } +} + +void +NodeModule::new_port_view(SPtr<const PortModel> port) +{ + Port::create(app(), *this, port); + + port->signal_value_changed().connect( + sigc::bind<0>(sigc::mem_fun(this, &NodeModule::port_value_changed), + port->index())); + + port->signal_activity().connect( + sigc::bind<0>(sigc::mem_fun(this, &NodeModule::port_activity), + port->index())); +} + +Port* +NodeModule::port(SPtr<const PortModel> model) +{ + for (iterator p = begin(); p != end(); ++p) { + Port* const port = dynamic_cast<Port*>(*p); + if (port->model() == model) { + return port; + } + } + return nullptr; +} + +void +NodeModule::delete_port_view(SPtr<const PortModel> model) +{ + Port* p = port(model); + if (p) { + delete p; + } else { + app().log().warn(fmt("Failed to find port %1% on module %2%\n") + % model->path() % _block->path()); + } +} + +bool +NodeModule::popup_gui() +{ + if (_block->plugin() && app().uris().lv2_Plugin == _block->plugin_model()->type()) { + if (_plugin_ui) { + app().log().warn("LV2 GUI already embedded, cannot pop up\n"); + return false; + } + + const PluginModel* const plugin = dynamic_cast<const PluginModel*>(_block->plugin()); + assert(plugin); + + _plugin_ui = plugin->ui(app().world(), _block); + + if (_plugin_ui) { + _plugin_ui->signal_property_changed().connect( + sigc::mem_fun(app(), &App::set_property)); + + if (!_plugin_ui->instantiated() && !_plugin_ui->instantiate()) { + app().log().error("Failed to instantiate LV2 UI\n"); + return false; + } + + GtkWidget* c_widget = (GtkWidget*)_plugin_ui->get_widget(); + _gui_widget = Glib::wrap(c_widget); + + _gui_window = new Gtk::Window(); + if (!_plugin_ui->is_resizable()) { + _gui_window->set_resizable(false); + } + _gui_window->set_title(_block->path() + " UI - Ingen"); + _gui_window->set_role("plugin_ui"); + _gui_window->add(*_gui_widget); + _gui_widget->show_all(); + set_control_values(); + + _gui_window->signal_unmap().connect( + sigc::mem_fun(this, &NodeModule::on_gui_window_close)); + _gui_window->present(); + + return true; + } else { + app().log().warn(fmt("No LV2 GUI for %1%\n") % _block->path()); + } + } + + return false; +} + +void +NodeModule::on_gui_window_close() +{ + delete _gui_window; + _gui_window = nullptr; + _plugin_ui.reset(); + _gui_widget = nullptr; +} + +void +NodeModule::set_control_values() +{ + uint32_t index = 0; + for (const auto& p : _block->ports()) { + if (app().can_control(p.get()) && p->value().is_valid()) { + port_value_changed(index, p->value()); + } + ++index; + } +} + +bool +NodeModule::on_double_click(GdkEventButton* event) +{ + popup_gui(); + return true; +} + +bool +NodeModule::on_event(GdkEvent* ev) +{ + if (ev->type == GDK_BUTTON_PRESS && ev->button.button == 3) { + return show_menu(&ev->button); + } else if (ev->type == GDK_2BUTTON_PRESS) { + return on_double_click(&ev->button); + } else if (ev->type == GDK_ENTER_NOTIFY) { + GraphBox* const box = app().window_factory()->graph_box( + dynamic_ptr_cast<const GraphModel>(_block->parent())); + if (box) { + box->object_entered(_block.get()); + } + } else if (ev->type == GDK_LEAVE_NOTIFY) { + GraphBox* const box = app().window_factory()->graph_box( + dynamic_ptr_cast<const GraphModel>(_block->parent())); + if (box) { + box->object_left(_block.get()); + } + } + + return false; +} + +void +NodeModule::store_location(double ax, double ay) +{ + const URIs& uris = app().uris(); + + const Atom x(app().forge().make(static_cast<float>(ax))); + const Atom y(app().forge().make(static_cast<float>(ay))); + + if (x != _block->get_property(uris.ingen_canvasX) || + y != _block->get_property(uris.ingen_canvasY)) + { + app().interface()->put(_block->uri(), {{uris.ingen_canvasX, x}, + {uris.ingen_canvasY, y}}); + } +} + +void +NodeModule::property_changed(const URI& key, const Atom& value) +{ + const URIs& uris = app().uris(); + if (value.type() == uris.forge.Float) { + if (key == uris.ingen_canvasX) { + move_to(value.get<float>(), get_y()); + } else if (key == uris.ingen_canvasY) { + move_to(get_x(), value.get<float>()); + } + } else if (value.type() == uris.forge.Bool) { + if (key == uris.ingen_polyphonic) { + set_stacked(value.get<int32_t>()); + } else if (key == uris.ingen_uiEmbedded && _initialised) { + if (value.get<int32_t>() && !_gui_widget) { + embed_gui(true); + } else if (!value.get<int32_t>() && _gui_widget) { + embed_gui(false); + } + } else if (key == uris.ingen_enabled) { + if (value.get<int32_t>()) { + set_dash_length(0.0); + } else { + set_dash_length(5.0); + } + } + } else if (value.type() == uris.forge.String) { + if (key == uris.lv2_name + && app().world()->conf().option("human-names").get<int32_t>()) { + set_label(value.ptr<char>()); + } + } +} + +bool +NodeModule::on_selected(gboolean selected) +{ + GraphWindow* win = app().window_factory()->parent_graph_window(block()); + if (!win) { + return true; + } + + if (selected && win->documentation_is_visible()) { + GraphWindow* win = app().window_factory()->parent_graph_window(block()); + std::string doc; + bool html = false; +#ifdef HAVE_WEBKIT + html = true; +#endif + if (block()->plugin_model()) { + doc = block()->plugin_model()->documentation(html); + } + win->set_documentation(doc, html); + } + + return true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/NodeModule.hpp b/src/gui/NodeModule.hpp new file mode 100644 index 00000000..863b6ffb --- /dev/null +++ b/src/gui/NodeModule.hpp @@ -0,0 +1,104 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_NODEMODULE_HPP +#define INGEN_GUI_NODEMODULE_HPP + +#include "ganv/Module.hpp" +#include "ingen/types.hpp" + +#include "Port.hpp" + +namespace Raul { class Atom; } + +namespace Ingen { namespace Client { +class BlockModel; +class PluginUI; +class PortModel; +} } + +namespace Ingen { +namespace GUI { + +class GraphCanvas; +class Port; +class NodeMenu; + +/** A module in a graphn. + * + * This base class is extended for various types of modules. + * + * \ingroup GUI + */ +class NodeModule : public Ganv::Module +{ +public: + static NodeModule* create( + GraphCanvas& canvas, + SPtr<const Client::BlockModel> block, + bool human); + + virtual ~NodeModule(); + + App& app() const; + + Port* port(SPtr<const Client::PortModel> model); + + void delete_port_view(SPtr<const Client::PortModel> model); + + virtual void store_location(double ax, double ay); + void show_human_names(bool b); + + SPtr<const Client::BlockModel> block() const { return _block; } + +protected: + NodeModule(GraphCanvas& canvas, SPtr<const Client::BlockModel> block); + + virtual bool on_double_click(GdkEventButton* ev); + + bool idle_init(); + bool on_event(GdkEvent* ev); + + void on_embed_gui_toggled(bool embed); + void embed_gui(bool embed); + bool popup_gui(); + void on_gui_window_close(); + bool on_selected(gboolean selected); + + void rename(); + void property_changed(const URI& key, const Atom& value); + + void new_port_view(SPtr<const Client::PortModel> port); + + void port_activity(uint32_t index, const Atom& value); + void port_value_changed(uint32_t index, const Atom& value); + void plugin_changed(); + void set_control_values(); + + bool show_menu(GdkEventButton* ev); + + SPtr<const Client::BlockModel> _block; + NodeMenu* _menu; + SPtr<Client::PluginUI> _plugin_ui; + Gtk::Widget* _gui_widget; + Gtk::Window* _gui_window; ///< iff popped up + bool _initialised; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_NODEMODULE_HPP diff --git a/src/gui/ObjectMenu.cpp b/src/gui/ObjectMenu.cpp new file mode 100644 index 00000000..bfce4248 --- /dev/null +++ b/src/gui/ObjectMenu.cpp @@ -0,0 +1,145 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <utility> + +#include "ingen/Forge.hpp" +#include "ingen/Interface.hpp" +#include "ingen/client/ObjectModel.hpp" + +#include "App.hpp" +#include "ObjectMenu.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +ObjectMenu::ObjectMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Gtk::Menu(cobject) + , _app(nullptr) + , _polyphonic_menuitem(nullptr) + , _disconnect_menuitem(nullptr) + , _rename_menuitem(nullptr) + , _destroy_menuitem(nullptr) + , _properties_menuitem(nullptr) + , _enable_signal(false) +{ + xml->get_widget("object_learn_menuitem", _learn_menuitem); + xml->get_widget("object_unlearn_menuitem", _unlearn_menuitem); + xml->get_widget("object_polyphonic_menuitem", _polyphonic_menuitem); + xml->get_widget("object_disconnect_menuitem", _disconnect_menuitem); + xml->get_widget("object_rename_menuitem", _rename_menuitem); + xml->get_widget("object_destroy_menuitem", _destroy_menuitem); + xml->get_widget("object_properties_menuitem", _properties_menuitem); + xml->get_widget("object_menu_separator", _separator_menuitem); +} + +void +ObjectMenu::init(App& app, SPtr<const ObjectModel> object) +{ + _app = &app; + _object = object; + + _polyphonic_menuitem->signal_toggled().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_polyphonic)); + + _polyphonic_menuitem->set_active(object->polyphonic()); + + _learn_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_learn)); + + _unlearn_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_unlearn)); + + _disconnect_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_disconnect)); + + _rename_menuitem->signal_activate().connect( + sigc::bind(sigc::mem_fun(_app->window_factory(), &WindowFactory::present_rename), + object)); + + _destroy_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_destroy)); + + _properties_menuitem->signal_activate().connect( + sigc::mem_fun(this, &ObjectMenu::on_menu_properties)); + + object->signal_property().connect(sigc::mem_fun(this, &ObjectMenu::property_changed)); + + _learn_menuitem->hide(); + _unlearn_menuitem->hide(); + + _enable_signal = true; +} + +void +ObjectMenu::on_menu_learn() +{ + _app->interface()->set_property(_object->uri(), + _app->uris().midi_binding, + _app->uris().patch_wildcard.urid); +} + +void +ObjectMenu::on_menu_unlearn() +{ + Properties remove; + remove.emplace(_app->uris().midi_binding, + Property(_app->uris().patch_wildcard)); + _app->interface()->delta(_object->uri(), remove, Properties()); +} + +void +ObjectMenu::on_menu_polyphonic() +{ + if (_enable_signal) { + _app->set_property( + _object->uri(), + _app->uris().ingen_polyphonic, + _app->forge().make(bool(_polyphonic_menuitem->get_active()))); + } +} + +void +ObjectMenu::property_changed(const URI& predicate, const Atom& value) +{ + const URIs& uris = _app->uris(); + _enable_signal = false; + if (predicate == uris.ingen_polyphonic && value.type() == uris.forge.Bool) { + _polyphonic_menuitem->set_active(value.get<int32_t>()); + } + _enable_signal = true; +} + +void +ObjectMenu::on_menu_destroy() +{ + _app->interface()->del(_object->uri()); +} + +void +ObjectMenu::on_menu_properties() +{ + _app->window_factory()->present_properties(_object); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/ObjectMenu.hpp b/src/gui/ObjectMenu.hpp new file mode 100644 index 00000000..a9b07fd5 --- /dev/null +++ b/src/gui/ObjectMenu.hpp @@ -0,0 +1,77 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_OBJECTMENU_HPP +#define INGEN_GUI_OBJECTMENU_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/checkmenuitem.h> +#include <gtkmm/menu.h> +#include <gtkmm/menuitem.h> + +#include "ingen/client/ObjectModel.hpp" +#include "ingen/types.hpp" + +namespace Ingen { +namespace GUI { + +class ObjectControlWindow; +class ObjectPropertiesWindow; +class GraphCanvas; + +/** Menu for a Object. + * + * \ingroup GUI + */ +class ObjectMenu : public Gtk::Menu +{ +public: + ObjectMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app, SPtr<const Client::ObjectModel> object); + + SPtr<const Client::ObjectModel> object() const { return _object; } + App* app() const { return _app; } + +protected: + void on_menu_learn(); + void on_menu_unlearn(); + virtual void on_menu_disconnect() = 0; + void on_menu_polyphonic(); + void on_menu_destroy(); + void on_menu_properties(); + + void property_changed(const URI& predicate, const Atom& value); + + App* _app; + SPtr<const Client::ObjectModel> _object; + Gtk::MenuItem* _learn_menuitem; + Gtk::MenuItem* _unlearn_menuitem; + Gtk::CheckMenuItem* _polyphonic_menuitem; + Gtk::MenuItem* _disconnect_menuitem; + Gtk::MenuItem* _rename_menuitem; + Gtk::MenuItem* _destroy_menuitem; + Gtk::MenuItem* _properties_menuitem; + Gtk::SeparatorMenuItem* _separator_menuitem; + + bool _enable_signal; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_OBJECTMENU_HPP diff --git a/src/gui/PluginMenu.cpp b/src/gui/PluginMenu.cpp new file mode 100644 index 00000000..fc385773 --- /dev/null +++ b/src/gui/PluginMenu.cpp @@ -0,0 +1,176 @@ +/* + This file is part of Ingen. + Copyright 2014-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "PluginMenu.hpp" +#include "ingen/Log.hpp" +#include "ingen/client/PluginModel.hpp" + +namespace Ingen { +namespace GUI { + +PluginMenu::PluginMenu(Ingen::World& world) + : _world(world) + , _classless_menu(nullptr, nullptr) +{ + clear(); +} + +void +PluginMenu::clear() +{ + const LilvWorld* lworld = _world.lilv_world(); + const LilvPluginClass* lv2_plugin = lilv_world_get_plugin_class(lworld); + const LilvPluginClasses* classes = lilv_world_get_plugin_classes(lworld); + + // Empty completely + _classless_menu = MenuRecord(nullptr, nullptr); + _class_menus.clear(); + items().clear(); + + // Build skeleton + LV2Children children; + LILV_FOREACH(plugin_classes, i, classes) { + const LilvPluginClass* c = lilv_plugin_classes_get(classes, i); + const LilvNode* p = lilv_plugin_class_get_parent_uri(c); + if (!p) { + p = lilv_plugin_class_get_uri(lv2_plugin); + } + children.emplace(lilv_node_as_string(p), c); + } + + std::set<const char*> ancestors; + build_plugin_class_menu(this, lv2_plugin, classes, children, ancestors); + + items().push_back(Gtk::Menu_Helpers::MenuElem("_Uncategorized")); + _classless_menu.item = &(items().back()); + _classless_menu.menu = Gtk::manage(new Gtk::Menu()); + _classless_menu.item->set_submenu(*_classless_menu.menu); + _classless_menu.item->hide(); +} + +void +PluginMenu::add_plugin(SPtr<Client::PluginModel> p) +{ + typedef ClassMenus::iterator iterator; + + if (!p->lilv_plugin() || lilv_plugin_is_replaced(p->lilv_plugin())) { + return; + } + + const LilvPluginClass* pc = lilv_plugin_get_class(p->lilv_plugin()); + const LilvNode* class_uri = lilv_plugin_class_get_uri(pc); + const char* class_uri_str = lilv_node_as_string(class_uri); + + std::pair<iterator, iterator> range = _class_menus.equal_range(class_uri_str); + if (range.first == _class_menus.end() || range.first == range.second + || range.first->second.menu == this) { + // Add to uncategorized plugin menu + add_plugin_to_menu(_classless_menu, p); + } else { + // For each menu that represents plugin's class (possibly several) + for (auto i = range.first; i != range.second ; ++i) { + add_plugin_to_menu(i->second, p); + } + } +} + +size_t +PluginMenu::build_plugin_class_menu(Gtk::Menu* menu, + const LilvPluginClass* plugin_class, + const LilvPluginClasses* classes, + const LV2Children& children, + std::set<const char*>& ancestors) +{ + size_t num_items = 0; + const LilvNode* class_uri = lilv_plugin_class_get_uri(plugin_class); + const char* class_uri_str = lilv_node_as_string(class_uri); + + const std::pair<LV2Children::const_iterator, LV2Children::const_iterator> kids + = children.equal_range(class_uri_str); + + if (kids.first == children.end()) { + return 0; + } + + // Add submenus + ancestors.insert(class_uri_str); + for (LV2Children::const_iterator i = kids.first; i != kids.second; ++i) { + const LilvPluginClass* c = i->second; + const char* sub_label_str = lilv_node_as_string(lilv_plugin_class_get_label(c)); + const char* sub_uri_str = lilv_node_as_string(lilv_plugin_class_get_uri(c)); + if (ancestors.find(sub_uri_str) != ancestors.end()) { + _world.log().warn(fmt("Infinite LV2 class recursion: %1% <: %2%\n") + % class_uri_str % sub_uri_str); + return 0; + } + + Gtk::Menu_Helpers::MenuElem menu_elem = Gtk::Menu_Helpers::MenuElem( + std::string("_") + sub_label_str); + menu->items().push_back(menu_elem); + Gtk::MenuItem* menu_item = &(menu->items().back()); + + Gtk::Menu* submenu = Gtk::manage(new Gtk::Menu()); + menu_item->set_submenu(*submenu); + + size_t num_child_items = build_plugin_class_menu( + submenu, c, classes, children, ancestors); + + _class_menus.emplace(sub_uri_str, MenuRecord(menu_item, submenu)); + if (num_child_items == 0) { + menu_item->hide(); + } + + ++num_items; + } + ancestors.erase(class_uri_str); + + return num_items; +} + +void +PluginMenu::add_plugin_to_menu(MenuRecord& menu, SPtr<Client::PluginModel> p) +{ + const URIs& uris = _world.uris(); + LilvWorld* lworld = _world.lilv_world(); + LilvNode* ingen_Graph = lilv_new_uri(lworld, uris.ingen_Graph.c_str()); + LilvNode* rdf_type = lilv_new_uri(lworld, uris.rdf_type.c_str()); + + bool is_graph = lilv_world_ask(lworld, + lilv_plugin_get_uri(p->lilv_plugin()), + rdf_type, + ingen_Graph); + + menu.menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + std::string("_") + p->human_name() + (is_graph ? " âš™" : ""), + sigc::bind(sigc::mem_fun(this, &PluginMenu::load_plugin), p))); + + if (!menu.item->is_visible()) { + menu.item->show(); + } + + lilv_node_free(rdf_type); + lilv_node_free(ingen_Graph); +} + +void +PluginMenu::load_plugin(WPtr<Client::PluginModel> weak_plugin) +{ + signal_load_plugin.emit(weak_plugin); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/PluginMenu.hpp b/src/gui/PluginMenu.hpp new file mode 100644 index 00000000..bc654db5 --- /dev/null +++ b/src/gui/PluginMenu.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2014-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_PLUGINMENU_HPP +#define INGEN_GUI_PLUGINMENU_HPP + +#include <map> +#include <set> +#include <string> + +#include <gtkmm/menu.h> + +#include "ingen/World.hpp" +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +namespace Ingen { + +namespace Client { class PluginModel; } + +namespace GUI { + +/** + Type-hierarchical plugin menu. + + @ingroup GUI +*/ +class PluginMenu : public Gtk::Menu +{ +public: + PluginMenu(Ingen::World& world); + + void clear(); + void add_plugin(SPtr<Client::PluginModel> p); + + sigc::signal< void, WPtr<Client::PluginModel> > signal_load_plugin; + +private: + struct MenuRecord { + MenuRecord(Gtk::MenuItem* i, Gtk::Menu* m) : item(i), menu(m) {} + Gtk::MenuItem* item; + Gtk::Menu* menu; + }; + + typedef std::multimap<const std::string, const LilvPluginClass*> LV2Children; + typedef std::multimap<const std::string, MenuRecord> ClassMenus; + + /// Recursively add hierarchy rooted at `plugin_class` to `menu`. + size_t build_plugin_class_menu(Gtk::Menu* menu, + const LilvPluginClass* plugin_class, + const LilvPluginClasses* classes, + const LV2Children& children, + std::set<const char*>& ancestors); + + void add_plugin_to_menu(MenuRecord& menu, SPtr<Client::PluginModel> p); + + void load_plugin(WPtr<Client::PluginModel> weak_plugin); + + Ingen::World& _world; + MenuRecord _classless_menu; + ClassMenus _class_menus; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_PLUGINMENU_HPP diff --git a/src/gui/Port.cpp b/src/gui/Port.cpp new file mode 100644 index 00000000..9742cee3 --- /dev/null +++ b/src/gui/Port.cpp @@ -0,0 +1,534 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> + +#include "ganv/Module.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PortModel.hpp" + +#include "App.hpp" +#include "GraphWindow.hpp" +#include "Port.hpp" +#include "PortMenu.hpp" +#include "RDFS.hpp" +#include "Style.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" +#include "ingen_config.h" +#include "rgba.hpp" + +using namespace Ingen::Client; + +namespace Ingen { +namespace GUI { + +Port* +Port::create(App& app, + Ganv::Module& module, + SPtr<const PortModel> pm, + bool flip) +{ + return new Port(app, module, pm, port_label(app, pm), flip); +} + +/** @param flip Make an input port appear as an output port, and vice versa. + */ +Port::Port(App& app, + Ganv::Module& module, + SPtr<const PortModel> pm, + const std::string& name, + bool flip) + : Ganv::Port(module, name, + flip ? (!pm->is_input()) : pm->is_input(), + app.style()->get_port_color(pm.get())) + , _app(app) + , _port_model(pm) + , _entered(false) + , _flipped(flip) +{ + assert(pm); + + if (app.can_control(pm.get())) { + show_control(); + pm->signal_value_changed().connect( + sigc::mem_fun(this, &Port::value_changed)); + } + + port_properties_changed(); + + pm->signal_property().connect( + sigc::mem_fun(this, &Port::property_changed)); + pm->signal_property_removed().connect( + sigc::mem_fun(this, &Port::property_removed)); + pm->signal_activity().connect( + sigc::mem_fun(this, &Port::activity)); + pm->signal_moved().connect( + sigc::mem_fun(this, &Port::moved)); + + signal_value_changed.connect( + sigc::mem_fun(this, &Port::on_value_changed)); + + signal_event().connect( + sigc::mem_fun(this, &Port::on_event)); + + set_is_controllable(pm->is_numeric() && pm->is_input()); + + Ganv::Port::set_beveled(model()->is_a(_app.uris().lv2_ControlPort) || + model()->has_property(_app.uris().atom_bufferType, + _app.uris().atom_Sequence)); + + for (const auto& p : pm->properties()) { + property_changed(p.first, p.second); + } + + update_metadata(); + value_changed(pm->value()); +} + +Port::~Port() +{ + _app.activity_port_destroyed(this); +} + +std::string +Port::port_label(App& app, SPtr<const PortModel> pm) +{ + if (!pm) { + return ""; + } + + std::string label; + if (app.world()->conf().option("port-labels").get<int32_t>()) { + if (app.world()->conf().option("human-names").get<int32_t>()) { + const Atom& name = pm->get_property(app.uris().lv2_name); + if (name.type() == app.forge().String) { + label = name.ptr<char>(); + } else { + const SPtr<const BlockModel> parent( + dynamic_ptr_cast<const BlockModel>(pm->parent())); + if (parent && parent->plugin_model()) { + label = parent->plugin_model()->port_human_name(pm->index()); + } + } + } else { + label = pm->path().symbol(); + } + } + return label; +} + +void +Port::ensure_label() +{ + if (!get_label()) { + set_label(port_label(_app, _port_model.lock()).c_str()); + } +} + +void +Port::update_metadata() +{ + SPtr<const PortModel> pm = _port_model.lock(); + if (pm && _app.can_control(pm.get()) && pm->is_numeric()) { + SPtr<const BlockModel> parent = dynamic_ptr_cast<const BlockModel>(pm->parent()); + if (parent) { + float min = 0.0f; + float max = 1.0f; + parent->port_value_range(pm, min, max, _app.sample_rate()); + set_control_min(min); + set_control_max(max); + } + } +} + +bool +Port::show_menu(GdkEventButton* ev) +{ + PortMenu* menu = nullptr; + WidgetFactory::get_widget_derived("object_menu", menu); + if (!menu) { + _app.log().error("Failed to load port menu widget\n"); + return false; + } + + menu->init(_app, model(), _flipped); + menu->popup(ev->button, ev->time); + return true; +} + +void +Port::moved() +{ + if (_app.world()->conf().option("port-labels").get<int32_t>() && + !_app.world()->conf().option("human-names").get<int32_t>()) { + set_label(model()->symbol().c_str()); + } +} + +void +Port::on_value_changed(double value) +{ + const URIs& uris = _app.uris(); + const Atom& current_value = model()->value(); + if (current_value.type() != uris.forge.Float) { + return; // Non-float, unsupported + } + + if (current_value.get<float>() == (float)value) { + return; // No change + } + + const Atom atom = _app.forge().make(float(value)); + _app.set_property(model()->uri(), + _app.world()->uris().ingen_value, + atom); + + if (_entered) { + GraphBox* box = get_graph_box(); + if (box) { + box->show_port_status(model().get(), atom); + } + } +} + +void +Port::value_changed(const Atom& value) +{ + if (value.type() == _app.forge().Float && !get_grabbed()) { + Ganv::Port::set_control_value(value.get<float>()); + } +} + +void +Port::on_scale_point_activated(float f) +{ + _app.set_property(model()->uri(), + _app.world()->uris().ingen_value, + _app.world()->forge().make(f)); +} + +Gtk::Menu* +Port::build_enum_menu() +{ + SPtr<const BlockModel> block = dynamic_ptr_cast<BlockModel>(model()->parent()); + Gtk::Menu* menu = Gtk::manage(new Gtk::Menu()); + + PluginModel::ScalePoints points = block->plugin_model()->port_scale_points( + model()->index()); + for (auto i = points.begin(); i != points.end(); ++i) { + menu->items().push_back(Gtk::Menu_Helpers::MenuElem(i->second)); + Gtk::MenuItem* menu_item = &(menu->items().back()); + menu_item->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &Port::on_scale_point_activated), + i->first)); + } + + return menu; +} + +void +Port::on_uri_activated(const URI& uri) +{ + _app.set_property(model()->uri(), + _app.world()->uris().ingen_value, + _app.world()->forge().make_urid( + _app.world()->uri_map().map_uri(uri.c_str()))); +} + +Gtk::Menu* +Port::build_uri_menu() +{ + World* world = _app.world(); + SPtr<const BlockModel> block = dynamic_ptr_cast<BlockModel>(model()->parent()); + Gtk::Menu* menu = Gtk::manage(new Gtk::Menu()); + + // Get the port designation, which should be a rdf:Property + const Atom& designation_atom = model()->get_property( + _app.uris().lv2_designation); + if (!designation_atom.is_valid()) { + return nullptr; + } + + LilvNode* designation = lilv_new_uri( + world->lilv_world(), world->forge().str(designation_atom, false).c_str()); + LilvNode* rdfs_range = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "range"); + + // Get every class in the range of the port's property + RDFS::URISet ranges; + LilvNodes* range = lilv_world_find_nodes( + world->lilv_world(), designation, rdfs_range, nullptr); + LILV_FOREACH(nodes, r, range) { + ranges.insert(URI(lilv_node_as_string(lilv_nodes_get(range, r)))); + } + RDFS::classes(world, ranges, false); + + // Get all objects in range + RDFS::Objects values = RDFS::instances(world, ranges); + + // Add a menu item for each such class + for (const auto& v : values) { + if (!v.first.empty()) { + const std::string qname = world->rdf_world()->prefixes().qualify(v.second); + const std::string label = qname + " - " + v.first; + menu->items().push_back(Gtk::Menu_Helpers::MenuElem(label)); + Gtk::MenuItem* menu_item = &(menu->items().back()); + menu_item->signal_activate().connect( + sigc::bind(sigc::mem_fun(this, &Port::on_uri_activated), + v.second)); + } + } + + return menu; +} + +bool +Port::on_event(GdkEvent* ev) +{ + GraphBox* box = nullptr; + switch (ev->type) { + case GDK_ENTER_NOTIFY: + _entered = true; + if ((box = get_graph_box())) { + box->object_entered(model().get()); + } + return false; + case GDK_LEAVE_NOTIFY: + _entered = false; + if ((box = get_graph_box())) { + box->object_left(model().get()); + } + return false; + case GDK_BUTTON_PRESS: + if (ev->button.button == 1) { + if (model()->is_enumeration()) { + Gtk::Menu* menu = build_enum_menu(); + menu->popup(ev->button.button, ev->button.time); + return true; + } else if (model()->is_uri()) { + Gtk::Menu* menu = build_uri_menu(); + if (menu) { + menu->popup(ev->button.button, ev->button.time); + return true; + } + } + } else if (ev->button.button == 3) { + return show_menu(&ev->button); + } + break; + default: + break; + } + + return false; +} + +inline static uint32_t +peak_color(float peak) +{ + static const uint32_t min = 0x4A8A0EC0; + static const uint32_t max = 0xFFCE1FC0; + static const uint32_t peak_min = 0xFF561FC0; + static const uint32_t peak_max = 0xFF0A38C0; + + if (peak < 1.0) { + return rgba_interpolate(min, max, peak); + } else { + return rgba_interpolate(peak_min, peak_max, fminf(peak, 2.0f) - 1.0f); + } +} + +void +Port::activity(const Atom& value) +{ + if (model()->is_a(_app.uris().lv2_AudioPort)) { + set_fill_color(peak_color(value.get<float>())); + } else if (_app.can_control(model().get()) && value.type() == _app.uris().atom_Float) { + Ganv::Port::set_control_value(value.get<float>()); + } else { + _app.port_activity(this); + } +} + +GraphBox* +Port::get_graph_box() const +{ + SPtr<const GraphModel> graph = dynamic_ptr_cast<const GraphModel>(model()->parent()); + GraphBox* box = _app.window_factory()->graph_box(graph); + if (!box) { + graph = dynamic_ptr_cast<const GraphModel>(model()->parent()->parent()); + box = _app.window_factory()->graph_box(graph); + } + return box; +} + +void +Port::set_type_tag() +{ + const URIs& uris = _app.uris(); + std::string tag; + if (model()->is_a(_app.uris().lv2_AudioPort)) { + tag = "~"; + } else if (model()->is_a(_app.uris().lv2_CVPort)) { + tag = "â„Ì°"; + } else if (model()->is_a(_app.uris().lv2_ControlPort)) { + if (model()->is_enumeration()) { + tag = "…"; + } else if (model()->is_integer()) { + tag = "ℤ"; + } else if (model()->is_toggle()) { + tag = ((model()->value() != _app.uris().forge.make(0.0f)) + ? "☑" : "â˜"); + + } else { + tag = "â„"; + } + } else if (model()->is_a(_app.uris().atom_AtomPort)) { + if (model()->supports(_app.uris().atom_Float)) { + if (model()->is_toggle()) { + tag = ((model()->value() != _app.uris().forge.make(0.0f)) + ? "☑" : "â˜"); + } else { + tag = "â„"; + } + } + if (model()->supports(_app.uris().atom_Int)) { + tag += "ℤ"; + } + if (model()->supports(_app.uris().midi_MidiEvent)) { + tag += "ð•„"; + } + if (model()->supports(_app.uris().patch_Message)) { + if (tag.empty()) { + tag += "="; + } else { + tag += "Ì¿"; + } + } + if (tag.empty()) { + tag = "*"; + } + + if (model()->has_property(uris.atom_bufferType, uris.atom_Sequence)) { + tag += "̤"; + } + } + + if (!tag.empty()) { + set_value_label(tag.c_str()); + } +} + +void +Port::port_properties_changed() +{ + if (model()->is_toggle()) { + set_control_is_toggle(true); + } else if (model()->is_integer()) { + set_control_is_integer(true); + } + set_type_tag(); +} + +void +Port::property_changed(const URI& key, const Atom& value) +{ + const URIs& uris = _app.uris(); + if (value.type() == uris.forge.Float) { + float val = value.get<float>(); + if (key == uris.ingen_value && !get_grabbed()) { + Ganv::Port::set_control_value(val); + if (model()->is_toggle()) { + std::string tag = (val == 0.0f) ? "â˜" : "☑"; + if (model()->is_a(_app.uris().lv2_CVPort)) { + tag += "Ì°"; + } else if (model()->has_property(uris.atom_bufferType, + uris.atom_Sequence)) { + tag += "̤"; + } + set_value_label(tag.c_str()); + } + } else if (key == uris.lv2_minimum) { + if (model()->port_property(uris.lv2_sampleRate)) { + val *= _app.sample_rate(); + } + set_control_min(val); + } else if (key == uris.lv2_maximum) { + if (model()->port_property(uris.lv2_sampleRate)) { + val *= _app.sample_rate(); + } + set_control_max(val); + } + } else if (key == uris.lv2_portProperty) { + port_properties_changed(); + } else if (key == uris.lv2_name) { + if (value.type() == uris.forge.String && + _app.world()->conf().option("port-labels").get<int32_t>() && + _app.world()->conf().option("human-names").get<int32_t>()) { + set_label(value.ptr<char>()); + } + } else if (key == uris.rdf_type || key == uris.atom_bufferType) { + set_fill_color(_app.style()->get_port_color(model().get())); + Ganv::Port::set_beveled(model()->is_a(uris.lv2_ControlPort) || + model()->has_property(uris.atom_bufferType, + uris.atom_Sequence)); + } +} + +void +Port::property_removed(const URI& key, const Atom& value) +{ + const URIs& uris = _app.uris(); + if (key == uris.lv2_minimum || key == uris.lv2_maximum) { + update_metadata(); + } else if (key == uris.rdf_type || key == uris.atom_bufferType) { + Ganv::Port::set_beveled(model()->is_a(uris.lv2_ControlPort) || + model()->has_property(uris.atom_bufferType, + uris.atom_Sequence)); + } +} + +bool +Port::on_selected(gboolean b) +{ + if (b) { + SPtr<const PortModel> pm = _port_model.lock(); + if (pm) { + SPtr<const BlockModel> block = dynamic_ptr_cast<const BlockModel>(pm->parent()); + GraphWindow* win = _app.window_factory()->parent_graph_window(block); + if (win && win->documentation_is_visible() && block->plugin_model()) { + bool html = false; +#ifdef HAVE_WEBKIT + html = true; +#endif + const std::string& doc = block->plugin_model()->port_documentation( + pm->index(), html); + win->set_documentation(doc, html); + } + } + } + + return true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/Port.hpp b/src/gui/Port.hpp new file mode 100644 index 00000000..c714feae --- /dev/null +++ b/src/gui/Port.hpp @@ -0,0 +1,102 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_PORT_HPP +#define INGEN_GUI_PORT_HPP + +#include <cassert> +#include <string> + +#include <gtkmm/menu.h> + +#include "ganv/Port.hpp" +#include "ingen/types.hpp" + +namespace Raul { +class Atom; +} + +namespace Ingen { + +class URI; + +namespace Client { class PortModel; } + +namespace GUI { + +class App; +class GraphBox; + +/** A Port on an Module. + * + * \ingroup GUI + */ +class Port : public Ganv::Port +{ +public: + static Port* create( + App& app, + Ganv::Module& module, + SPtr<const Client::PortModel> pm, + bool flip = false); + + ~Port(); + + SPtr<const Client::PortModel> model() const { return _port_model.lock(); } + + bool show_menu(GdkEventButton* ev); + void update_metadata(); + void ensure_label(); + + void value_changed(const Atom& value); + void activity(const Atom& value); + + bool on_selected(gboolean b); + +private: + Port(App& app, + Ganv::Module& module, + SPtr<const Client::PortModel> pm, + const std::string& name, + bool flip = false); + + static std::string port_label(App& app, SPtr<const Client::PortModel> pm); + + Gtk::Menu* build_enum_menu(); + Gtk::Menu* build_uri_menu(); + GraphBox* get_graph_box() const; + + void property_changed(const URI& key, const Atom& value); + void property_removed(const URI& key, const Atom& value); + void moved(); + + void on_value_changed(double value); + void on_scale_point_activated(float f); + void on_uri_activated(const URI& uri); + bool on_event(GdkEvent* ev); + void port_properties_changed(); + void set_type_tag(); + + App& _app; + WPtr<const Client::PortModel> _port_model; + bool _entered : 1; + bool _flipped : 1; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_PORT_HPP diff --git a/src/gui/PortMenu.cpp b/src/gui/PortMenu.cpp new file mode 100644 index 00000000..c6ec8fa1 --- /dev/null +++ b/src/gui/PortMenu.cpp @@ -0,0 +1,174 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/Interface.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/PortModel.hpp" +#include "ingen/types.hpp" + +#include "App.hpp" +#include "PortMenu.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +PortMenu::PortMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : ObjectMenu(cobject, xml) + , _internal_graph_port(false) +{ + xml->get_widget("object_menu", _port_menu); + xml->get_widget("port_set_min_menuitem", _set_min_menuitem); + xml->get_widget("port_set_max_menuitem", _set_max_menuitem); + xml->get_widget("port_reset_range_menuitem", _reset_range_menuitem); + xml->get_widget("port_expose_menuitem", _expose_menuitem); +} + +void +PortMenu::init(App& app, SPtr<const PortModel> port, bool internal_graph_port) +{ + const URIs& uris = app.uris(); + + ObjectMenu::init(app, port); + _internal_graph_port = internal_graph_port; + + _set_min_menuitem->signal_activate().connect( + sigc::mem_fun(this, &PortMenu::on_menu_set_min)); + + _set_max_menuitem->signal_activate().connect( + sigc::mem_fun(this, &PortMenu::on_menu_set_max)); + + _reset_range_menuitem->signal_activate().connect( + sigc::mem_fun(this, &PortMenu::on_menu_reset_range)); + + _expose_menuitem->signal_activate().connect( + sigc::mem_fun(this, &PortMenu::on_menu_expose)); + + const bool is_control(app.can_control(port.get()) && port->is_numeric()); + const bool is_on_graph(dynamic_ptr_cast<GraphModel>(port->parent())); + const bool is_input(port->is_input()); + + if (!is_on_graph) { + _polyphonic_menuitem->set_sensitive(false); + _rename_menuitem->set_sensitive(false); + _destroy_menuitem->set_sensitive(false); + } + + if (port->is_a(uris.atom_AtomPort)) { + _polyphonic_menuitem->hide(); + } + + _reset_range_menuitem->set_visible(is_input && is_control && !is_on_graph); + _set_max_menuitem->set_visible(is_input && is_control); + _set_min_menuitem->set_visible(is_input && is_control); + _expose_menuitem->set_visible(!is_on_graph); + _learn_menuitem->set_visible(is_input && is_control); + _unlearn_menuitem->set_visible(is_input && is_control); + + if (!is_control && is_on_graph) { + _separator_menuitem->hide(); + } + + _enable_signal = true; +} + +void +PortMenu::on_menu_disconnect() +{ + if (_internal_graph_port) { + _app->interface()->disconnect_all( + _object->parent()->path(), _object->path()); + } else { + _app->interface()->disconnect_all( + _object->parent()->path().parent(), _object->path()); + } +} + +void +PortMenu::on_menu_set_min() +{ + const URIs& uris = _app->uris(); + SPtr<const PortModel> model = dynamic_ptr_cast<const PortModel>(_object); + const Atom& value = model->get_property(uris.ingen_value); + if (value.is_valid()) { + _app->set_property(_object->uri(), uris.lv2_minimum, value); + } +} + +void +PortMenu::on_menu_set_max() +{ + const URIs& uris = _app->uris(); + SPtr<const PortModel> model = dynamic_ptr_cast<const PortModel>(_object); + const Atom& value = model->get_property(uris.ingen_value); + if (value.is_valid()) { + _app->set_property(_object->uri(), uris.lv2_maximum, value); + } +} + +void +PortMenu::on_menu_reset_range() +{ + const URIs& uris = _app->uris(); + SPtr<const PortModel> model = dynamic_ptr_cast<const PortModel>(_object); + + // Remove lv2:minimum and lv2:maximum properties + Properties remove; + remove.insert({uris.lv2_minimum, Property(uris.patch_wildcard)}); + remove.insert({uris.lv2_maximum, Property(uris.patch_wildcard)}); + _app->interface()->delta(_object->uri(), remove, Properties()); +} + +void +PortMenu::on_menu_expose() +{ + const URIs& uris = _app->uris(); + SPtr<const PortModel> port = dynamic_ptr_cast<const PortModel>(_object); + SPtr<const BlockModel> block = dynamic_ptr_cast<const BlockModel>(port->parent()); + + const std::string label = block->label() + " " + block->port_label(port); + const Raul::Path path = Raul::Path(block->path() + Raul::Symbol("_" + port->symbol())); + + Ingen::Resource r(*_object.get()); + r.remove_property(uris.lv2_index, uris.patch_wildcard); + r.set_property(uris.lv2_symbol, _app->forge().alloc(path.symbol())); + r.set_property(uris.lv2_name, _app->forge().alloc(label.c_str())); + + // TODO: Pretty kludgey coordinates + const float block_x = block->get_property(uris.ingen_canvasX).get<float>(); + const float block_y = block->get_property(uris.ingen_canvasY).get<float>(); + const float x_off = (label.length() * 16.0f) * (port->is_input() ? -1 : 1); + const float y_off = port->index() * 32.0f; + r.set_property(uris.ingen_canvasX, _app->forge().make(block_x + x_off)); + r.set_property(uris.ingen_canvasY, _app->forge().make(block_y + y_off)); + + _app->interface()->put(path_to_uri(path), r.properties()); + + if (port->is_input()) { + _app->interface()->connect(path, _object->path()); + } else { + _app->interface()->connect(_object->path(), path); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/PortMenu.hpp b/src/gui/PortMenu.hpp new file mode 100644 index 00000000..db567980 --- /dev/null +++ b/src/gui/PortMenu.hpp @@ -0,0 +1,66 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_PORTMENU_HPP +#define INGEN_GUI_PORTMENU_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/menu.h> +#include <gtkmm/menushell.h> + +#include "ingen/client/PortModel.hpp" +#include "ingen/types.hpp" + +#include "ObjectMenu.hpp" + +namespace Ingen { +namespace GUI { + +/** Menu for a Port. + * + * \ingroup GUI + */ +class PortMenu : public ObjectMenu +{ +public: + PortMenu(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void init(App& app, + SPtr<const Client::PortModel> port, + bool internal_graph_port = false); + +private: + void on_menu_disconnect(); + void on_menu_set_min(); + void on_menu_set_max(); + void on_menu_reset_range(); + void on_menu_expose(); + + Gtk::Menu* _port_menu; + Gtk::MenuItem* _set_min_menuitem; + Gtk::MenuItem* _set_max_menuitem; + Gtk::MenuItem* _reset_range_menuitem; + Gtk::MenuItem* _expose_menuitem; + + /// True iff this is a (flipped) port on a GraphPortModule in its graph + bool _internal_graph_port; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_PORTMENU_HPP diff --git a/src/gui/PropertiesWindow.cpp b/src/gui/PropertiesWindow.cpp new file mode 100644 index 00000000..4d47b3ae --- /dev/null +++ b/src/gui/PropertiesWindow.cpp @@ -0,0 +1,591 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <algorithm> +#include <cassert> +#include <set> + +#include <gtkmm/label.h> +#include <gtkmm/spinbutton.h> + +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "ingen/client/BlockModel.hpp" +#include "ingen/client/PluginModel.hpp" + +#include "App.hpp" +#include "PropertiesWindow.hpp" +#include "RDFS.hpp" +#include "URIEntry.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +typedef std::set<URI> URISet; + +PropertiesWindow::PropertiesWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) + , _value_type(0) +{ + xml->get_widget("properties_vbox", _vbox); + xml->get_widget("properties_scrolledwindow", _scrolledwindow); + xml->get_widget("properties_table", _table); + xml->get_widget("properties_key_combo", _key_combo); + xml->get_widget("properties_value_bin", _value_bin); + xml->get_widget("properties_add_button", _add_button); + xml->get_widget("properties_cancel_button", _cancel_button); + xml->get_widget("properties_apply_button", _apply_button); + xml->get_widget("properties_ok_button", _ok_button); + + _key_store = Gtk::ListStore::create(_combo_columns); + _key_combo->set_model(_key_store); + _key_combo->pack_start(_combo_columns.label_col); + + _key_combo->signal_changed().connect( + sigc::mem_fun(this, &PropertiesWindow::key_changed)); + + _add_button->signal_clicked().connect( + sigc::mem_fun(this, &PropertiesWindow::add_clicked)); + + _cancel_button->signal_clicked().connect( + sigc::mem_fun(this, &PropertiesWindow::cancel_clicked)); + + _apply_button->signal_clicked().connect( + sigc::mem_fun(this, &PropertiesWindow::apply_clicked)); + + _ok_button->signal_clicked().connect( + sigc::mem_fun(this, &PropertiesWindow::ok_clicked)); +} + +void +PropertiesWindow::reset() +{ + _property_connection.disconnect(); + _property_removed_connection.disconnect(); + + _key_store->clear(); + _records.clear(); + + _model.reset(); + + _table->children().clear(); + _table->resize(1, 3); + _table->property_n_rows() = 1; +} + +void +PropertiesWindow::present(SPtr<const ObjectModel> model) +{ + set_object(model); + Gtk::Window::present(); +} + +void +PropertiesWindow::add_property(const URI& key, const Atom& value) +{ + World* world = _app->world(); + + const unsigned n_rows = _table->property_n_rows() + 1; + _table->property_n_rows() = n_rows; + + // Column 0: Property + LilvNode* prop = lilv_new_uri(world->lilv_world(), key.c_str()); + std::string name = RDFS::label(world, prop); + if (name.empty()) { + name = world->rdf_world()->prefixes().qualify(key); + } + Gtk::Label* label = new Gtk::Label( + std::string("<a href=\"") + key.string() + "\">" + name + "</a>", + 1.0, + 0.5); + label->set_use_markup(true); + _app->set_tooltip(label, prop); + _table->attach(*Gtk::manage(label), 0, 1, n_rows, n_rows + 1, + Gtk::FILL|Gtk::SHRINK, Gtk::SHRINK); + + // Column 1: Value + Gtk::Alignment* align = manage(new Gtk::Alignment(0.0, 0.5, 1.0, 1.0)); + Gtk::CheckButton* present = manage(new Gtk::CheckButton()); + const char* type = _app->world()->uri_map().unmap_uri(value.type()); + Gtk::Widget* val_widget = create_value_widget(key, type, value); + + present->set_active(); + if (val_widget) { + align->add(*Gtk::manage(val_widget)); + _app->set_tooltip(val_widget, prop); + } + + _table->attach(*align, 1, 2, n_rows, n_rows + 1, + Gtk::FILL|Gtk::EXPAND, Gtk::SHRINK); + _table->attach(*present, 2, 3, n_rows, n_rows + 1, + Gtk::FILL, Gtk::SHRINK); + _records.emplace(key, Record(value, align, n_rows, present)); + _table->show_all(); + + lilv_node_free(prop); +} + +bool +PropertiesWindow::datatype_supported(const RDFS::URISet& types, + URI* widget_type) +{ + if (types.find(_app->uris().atom_Int) != types.end()) { + *widget_type = _app->uris().atom_Int; + return true; + } else if (types.find(_app->uris().atom_Float) != types.end()) { + *widget_type = _app->uris().atom_Float; + return true; + } else if (types.find(_app->uris().atom_Bool) != types.end()) { + *widget_type = _app->uris().atom_Bool; + return true; + } else if (types.find(_app->uris().atom_String) != types.end()) { + *widget_type = _app->uris().atom_String; + return true; + } else if (types.find(_app->uris().atom_URID) != types.end()) { + *widget_type = _app->uris().atom_URID; + return true; + } + + return false; +} + +bool +PropertiesWindow::class_supported(const RDFS::URISet& types) +{ + World* world = _app->world(); + LilvNode* rdf_type = lilv_new_uri( + world->lilv_world(), LILV_NS_RDF "type"); + + for (const auto& t : types) { + LilvNode* range = lilv_new_uri(world->lilv_world(), t.c_str()); + LilvNodes* instances = lilv_world_find_nodes( + world->lilv_world(), nullptr, rdf_type, range); + + const bool has_instance = (lilv_nodes_size(instances) > 0); + + lilv_nodes_free(instances); + lilv_node_free(range); + if (has_instance) { + lilv_node_free(rdf_type); + return true; + } + } + + lilv_node_free(rdf_type); + return false; +} + +/** Set the node this window is associated with. + * This function MUST be called before using this object in any way. + */ +void +PropertiesWindow::set_object(SPtr<const ObjectModel> model) +{ + reset(); + _model = model; + + set_title(model->path() + " Properties - Ingen"); + + World* world = _app->world(); + + LilvNode* rdf_type = lilv_new_uri( + world->lilv_world(), LILV_NS_RDF "type"); + LilvNode* rdfs_DataType = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "Datatype"); + + // Populate key combo + const URISet props = RDFS::properties(world, model); + std::map<std::string, URI> entries; + for (const auto& p : props) { + LilvNode* prop = lilv_new_uri(world->lilv_world(), p.c_str()); + const std::string label = RDFS::label(world, prop); + URISet ranges = RDFS::range(world, prop, true); + + lilv_node_free(prop); + if (label.empty() || ranges.empty()) { + // Property has no label or range, can't show a widget for it + continue; + } + + LilvNode* range = lilv_new_uri(world->lilv_world(), (*ranges.begin()).c_str()); + if (RDFS::is_a(world, range, rdfs_DataType)) { + // Range is a datatype, show if type or any subtype is supported + RDFS::datatypes(_app->world(), ranges, false); + URI widget_type("urn:nothing"); + if (datatype_supported(ranges, &widget_type)) { + entries.emplace(label, p); + } + } else { + // Range is presumably a class, show if any instances are known + if (class_supported(ranges)) { + entries.emplace(label, p); + } + } + } + + for (const auto& e : entries) { + Gtk::ListStore::iterator ki = _key_store->append(); + Gtk::ListStore::Row row = *ki; + row[_combo_columns.uri_col] = e.second.string(); + row[_combo_columns.label_col] = e.first; + } + + lilv_node_free(rdfs_DataType); + lilv_node_free(rdf_type); + + for (const auto& p : model->properties()) { + add_property(p.first, p.second); + } + + _table->show_all(); + + _property_connection = model->signal_property().connect( + sigc::mem_fun(this, &PropertiesWindow::add_property)); + _property_removed_connection = model->signal_property_removed().connect( + sigc::mem_fun(this, &PropertiesWindow::remove_property)); +} + +Gtk::Widget* +PropertiesWindow::create_value_widget(const URI& key, + const char* type_uri, + const Atom& value) +{ + if (!type_uri || !URI::is_valid(type_uri)) { + return nullptr; + } + + URI type(type_uri); + Ingen::World* world = _app->world(); + LilvWorld* lworld = world->lilv_world(); + + // See if type is a datatype we support + std::set<URI> types{type}; + RDFS::datatypes(_app->world(), types, false); + + URI widget_type("urn:nothing"); + const bool supported = datatype_supported(types, &widget_type); + if (supported) { + type = widget_type; + _value_type = _app->world()->uri_map().map_uri(type); + } + + if (type == _app->uris().atom_Int) { + Gtk::SpinButton* widget = manage(new Gtk::SpinButton(0.0, 0)); + widget->property_numeric() = true; + widget->set_range(INT_MIN, INT_MAX); + widget->set_increments(1, 10); + if (value.is_valid()) { + widget->set_value(value.get<int32_t>()); + } + widget->signal_value_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + return widget; + } else if (type == _app->uris().atom_Float) { + Gtk::SpinButton* widget = manage(new Gtk::SpinButton(0.0, 4)); + widget->property_numeric() = true; + widget->set_snap_to_ticks(false); + widget->set_range(-FLT_MAX, FLT_MAX); + widget->set_increments(0.1, 1.0); + if (value.is_valid()) { + widget->set_value(value.get<float>()); + } + widget->signal_value_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + return widget; + } else if (type == _app->uris().atom_Bool) { + Gtk::CheckButton* widget = manage(new Gtk::CheckButton()); + if (value.is_valid()) { + widget->set_active(value.get<int32_t>()); + } + widget->signal_toggled().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + return widget; + } else if (type == _app->uris().atom_String) { + Gtk::Entry* widget = manage(new Gtk::Entry()); + if (value.is_valid()) { + widget->set_text(value.ptr<char>()); + } + widget->signal_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + return widget; + } else if (type == _app->uris().atom_URID) { + const char* str = (value.is_valid() + ? world->uri_map().unmap_uri(value.get<int32_t>()) + : ""); + + LilvNode* pred = lilv_new_uri(lworld, key.c_str()); + URISet ranges = RDFS::range(world, pred, true); + URIEntry* widget = manage(new URIEntry(_app, ranges, str ? str : "")); + widget->signal_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + lilv_node_free(pred); + return widget; + } + + LilvNode* type_node = lilv_new_uri(lworld, type.c_str()); + LilvNode* rdfs_Class = lilv_new_uri(lworld, LILV_NS_RDFS "Class"); + const bool is_class = RDFS::is_a(world, type_node, rdfs_Class); + lilv_node_free(rdfs_Class); + lilv_node_free(type_node); + + if (type == _app->uris().atom_URI || + type == _app->uris().rdfs_Class || + is_class) { + LilvNode* pred = lilv_new_uri(lworld, key.c_str()); + URISet ranges = RDFS::range(world, pred, true); + const char* str = value.is_valid() ? value.ptr<const char>() : ""; + URIEntry* widget = manage(new URIEntry(_app, ranges, str)); + widget->signal_changed().connect( + sigc::bind(sigc::mem_fun(this, &PropertiesWindow::on_change), key)); + lilv_node_free(pred); + return widget; + } + + _app->log().error(fmt("No widget for value type %1%\n") % type); + + return nullptr; +} + +void +PropertiesWindow::on_show() +{ + static const int WIN_PAD = 64; + static const int VBOX_PAD = 16; + + int width = 0; + int height = 0; + + for (const auto& c : _vbox->children()) { + const Gtk::Requisition& req = c.get_widget()->size_request(); + + width = std::max(width, req.width); + height += req.height + VBOX_PAD; + } + + const Gtk::Requisition& req = _table->size_request(); + + width = 1.2 * std::max(width, req.width + 128); + height += req.height; + + set_default_size(width + WIN_PAD, height + WIN_PAD); + resize(width + WIN_PAD, height + WIN_PAD); + Gtk::Window::on_show(); +} + +void +PropertiesWindow::change_property(const URI& key, const Atom& value) +{ + auto r = _records.find(key); + if (r == _records.end()) { + add_property(key, value); + _table->show_all(); + return; + } + + Record& record = r->second; + const char* type = _app->world()->uri_map().unmap_uri(value.type()); + Gtk::Widget* val_widget = create_value_widget(key, type, value); + + if (val_widget) { + record.value_widget->remove(); + record.value_widget->add(*Gtk::manage(val_widget)); + val_widget->show_all(); + } + + record.value = value; +} + +void +PropertiesWindow::remove_property(const URI& key, const Atom& value) +{ + // Bleh, there doesn't seem to be an easy way to remove a Gtk::Table row... + _records.clear(); + _table->children().clear(); + _table->resize(1, 3); + _table->property_n_rows() = 1; + + for (const auto& p : _model->properties()) { + add_property(p.first, p.second); + } + _table->show_all(); +} + +Atom +PropertiesWindow::get_value(LV2_URID type, Gtk::Widget* value_widget) +{ + Forge& forge = _app->forge(); + + if (type == forge.Int) { + Gtk::SpinButton* spin = dynamic_cast<Gtk::SpinButton*>(value_widget); + if (spin) { + return _app->forge().make(spin->get_value_as_int()); + } + } else if (type == forge.Float) { + Gtk::SpinButton* spin = dynamic_cast<Gtk::SpinButton*>(value_widget); + if (spin) { + return _app->forge().make(static_cast<float>(spin->get_value())); + } + } else if (type == forge.Bool) { + Gtk::CheckButton* check = dynamic_cast<Gtk::CheckButton*>(value_widget); + if (check) { + return _app->forge().make(check->get_active()); + } + } else if (type == forge.URI || type == forge.URID) { + URIEntry* uri_entry = dynamic_cast<URIEntry*>(value_widget); + if (uri_entry && URI::is_valid(uri_entry->get_text())) { + return _app->forge().make_urid(URI(uri_entry->get_text())); + } else { + _app->log().error(fmt("Invalid URI <%1%>\n") % uri_entry->get_text()); + } + } else if (type == forge.String) { + Gtk::Entry* entry = dynamic_cast<Gtk::Entry*>(value_widget); + if (entry) { + return _app->forge().alloc(entry->get_text()); + } + } + + return Atom(); +} + +void +PropertiesWindow::on_change(const URI& key) +{ + auto r = _records.find(key); + if (r == _records.end()) { + return; + } + + Record& record = r->second; + const Atom value = get_value(record.value.type(), + record.value_widget->get_child()); + + if (value.is_valid()) { + record.value = value; + } else { + _app->log().error(fmt("Failed to get `%1%' value from widget\n") % key); + } +} + +std::string +PropertiesWindow::active_key() const +{ + const Gtk::ListStore::iterator iter = _key_combo->get_active(); + if (!iter) { + return ""; + } + + Glib::ustring prop_uri = (*iter)[_combo_columns.uri_col]; + return prop_uri; +} + +void +PropertiesWindow::key_changed() +{ + _value_bin->remove(); + if (!_key_combo->get_active()) { + return; + } + + LilvWorld* lworld = _app->world()->lilv_world(); + const Gtk::ListStore::Row key_row = *(_key_combo->get_active()); + const Glib::ustring key_uri = key_row[_combo_columns.uri_col]; + LilvNode* prop = lilv_new_uri(lworld, key_uri.c_str()); + + // Try to create a value widget in the range of this property + const URISet ranges = RDFS::range(_app->world(), prop, true); + for (const auto& r : ranges) { + Gtk::Widget* value_widget = create_value_widget( + URI(key_uri), r.c_str(), Atom()); + + if (value_widget) { + _add_button->set_sensitive(true); + _value_bin->remove(); + _value_bin->add(*Gtk::manage(value_widget)); + _value_bin->show_all(); + break; + } + } + + lilv_node_free(prop); +} + +void +PropertiesWindow::add_clicked() +{ + if (!_key_combo->get_active() || !_value_type || !_value_bin->get_child()) { + return; + } + + // Get selected key URI + const Gtk::ListStore::Row key_row = *(_key_combo->get_active()); + const Glib::ustring key_uri = key_row[_combo_columns.uri_col]; + + // Try to get value from value widget + const Atom& value = get_value(_value_type, _value_bin->get_child()); + if (value.is_valid()) { + // Send property to engine + Properties properties; + properties.emplace(URI(key_uri.c_str()), Property(value)); + _app->interface()->put(_model->uri(), properties); + } +} + +void +PropertiesWindow::cancel_clicked() +{ + reset(); + Gtk::Window::hide(); +} + +void +PropertiesWindow::apply_clicked() +{ + Properties remove; + Properties add; + for (const auto& r : _records) { + const URI& uri = r.first; + const Record& record = r.second; + if (record.present_button->get_active()) { + if (!_model->has_property(uri, record.value)) { + add.emplace(uri, record.value); + } + } else { + remove.emplace(uri, record.value); + } + } + + if (remove.empty()) { + _app->interface()->put(_model->uri(), add); + } else { + _app->interface()->delta(_model->uri(), remove, add); + } +} + +void +PropertiesWindow::ok_clicked() +{ + apply_clicked(); + Gtk::Window::hide(); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/PropertiesWindow.hpp b/src/gui/PropertiesWindow.hpp new file mode 100644 index 00000000..f4a8dd0d --- /dev/null +++ b/src/gui/PropertiesWindow.hpp @@ -0,0 +1,129 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_PROPERTIES_WINDOW_HPP +#define INGEN_GUI_PROPERTIES_WINDOW_HPP + +#include <map> + +#include <gtkmm/alignment.h> +#include <gtkmm/box.h> +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/checkbutton.h> +#include <gtkmm/combobox.h> +#include <gtkmm/liststore.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/table.h> + +#include "ingen/client/BlockModel.hpp" +#include "ingen/types.hpp" + +#include "Window.hpp" + +namespace Ingen { + +namespace Client { class ObjectModel; } + +namespace GUI { + +/** Object properties window. + * + * Loaded from XML as a derived object. + * + * \ingroup GUI + */ +class PropertiesWindow : public Window +{ +public: + PropertiesWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void present(SPtr<const Client::ObjectModel> model); + void set_object(SPtr<const Client::ObjectModel> model); + +private: + /** Record of a property (row in the table) */ + struct Record { + Record(const Atom& v, Gtk::Alignment* vw, int r, Gtk::CheckButton* cb) + : value(v), value_widget(vw), row(r), present_button(cb) + {} + Atom value; + Gtk::Alignment* value_widget; + int row; + Gtk::CheckButton* present_button; + }; + + struct ComboColumns : public Gtk::TreeModel::ColumnRecord { + ComboColumns() { + add(label_col); + add(uri_col); + } + Gtk::TreeModelColumn<Glib::ustring> label_col; + Gtk::TreeModelColumn<Glib::ustring> uri_col; + }; + + void add_property(const URI& key, const Atom& value); + void change_property(const URI& key, const Atom& value); + void remove_property(const URI& key, const Atom& value); + void on_change(const URI& key); + + bool datatype_supported(const std::set<URI>& types, + URI* widget_type); + + bool class_supported(const std::set<URI>& types); + + Gtk::Widget* create_value_widget(const URI& key, + const char* type_uri, + const Atom& value = Atom()); + + Atom get_value(LV2_URID type, Gtk::Widget* value_widget); + + void reset(); + void on_show(); + + std::string active_key() const; + + void key_changed(); + void add_clicked(); + void cancel_clicked(); + void apply_clicked(); + void ok_clicked(); + + typedef std::map<URI, Record> Records; + Records _records; + + SPtr<const Client::ObjectModel> _model; + ComboColumns _combo_columns; + Glib::RefPtr<Gtk::ListStore> _key_store; + sigc::connection _property_connection; + sigc::connection _property_removed_connection; + Gtk::VBox* _vbox; + Gtk::ScrolledWindow* _scrolledwindow; + Gtk::Table* _table; + Gtk::ComboBox* _key_combo; + LV2_URID _value_type; + Gtk::Bin* _value_bin; + Gtk::Button* _add_button; + Gtk::Button* _cancel_button; + Gtk::Button* _apply_button; + Gtk::Button* _ok_button; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_PROPERTIES_WINDOW_HPP diff --git a/src/gui/RDFS.cpp b/src/gui/RDFS.cpp new file mode 100644 index 00000000..71b3441a --- /dev/null +++ b/src/gui/RDFS.cpp @@ -0,0 +1,259 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/Log.hpp" +#include "ingen/Resource.hpp" +#include "ingen/World.hpp" +#include "ingen/client/ObjectModel.hpp" +#include "lilv/lilv.h" + +#include "RDFS.hpp" + +namespace Ingen { +namespace GUI { +namespace RDFS { + +std::string +label(World* world, const LilvNode* node) +{ + LilvNode* rdfs_label = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "label"); + LilvNodes* labels = lilv_world_find_nodes( + world->lilv_world(), node, rdfs_label, nullptr); + + const LilvNode* first = lilv_nodes_get_first(labels); + std::string label = first ? lilv_node_as_string(first) : ""; + + lilv_nodes_free(labels); + lilv_node_free(rdfs_label); + return label; +} + +std::string +comment(World* world, const LilvNode* node) +{ + LilvNode* rdfs_comment = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "comment"); + LilvNodes* comments = lilv_world_find_nodes( + world->lilv_world(), node, rdfs_comment, nullptr); + + const LilvNode* first = lilv_nodes_get_first(comments); + std::string comment = first ? lilv_node_as_string(first) : ""; + + lilv_nodes_free(comments); + lilv_node_free(rdfs_comment); + return comment; +} + +static void +closure(World* world, const LilvNode* pred, URISet& types, bool super) +{ + unsigned added = 0; + do { + added = 0; + URISet klasses; + for (const auto& t : types) { + LilvNode* type = lilv_new_uri(world->lilv_world(), t.c_str()); + LilvNodes* matches = (super) + ? lilv_world_find_nodes( + world->lilv_world(), type, pred, nullptr) + : lilv_world_find_nodes( + world->lilv_world(), nullptr, pred, type); + LILV_FOREACH(nodes, m, matches) { + const LilvNode* klass_node = lilv_nodes_get(matches, m); + if (lilv_node_is_uri(klass_node)) { + URI klass(lilv_node_as_uri(klass_node)); + if (!types.count(klass)) { + ++added; + klasses.insert(klass); + } + } + } + lilv_nodes_free(matches); + lilv_node_free(type); + } + types.insert(klasses.begin(), klasses.end()); + } while (added > 0); +} + +void +classes(World* world, URISet& types, bool super) +{ + LilvNode* rdfs_subClassOf = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "subClassOf"); + + closure(world, rdfs_subClassOf, types, super); + + lilv_node_free(rdfs_subClassOf); +} + +void +datatypes(World* world, URISet& types, bool super) +{ + LilvNode* owl_onDatatype = lilv_new_uri( + world->lilv_world(), LILV_NS_OWL "onDatatype"); + + closure(world, owl_onDatatype, types, super); + + lilv_node_free(owl_onDatatype); +} + +URISet +types(World* world, SPtr<const Client::ObjectModel> model) +{ + typedef Properties::const_iterator PropIter; + typedef std::pair<PropIter, PropIter> PropRange; + + // Start with every rdf:type + URISet types; + types.insert(URI(LILV_NS_RDFS "Resource")); + PropRange range = model->properties().equal_range(world->uris().rdf_type); + for (auto t = range.first; t != range.second; ++t) { + if (t->second.type() == world->forge().URI || + t->second.type() == world->forge().URID) { + const URI type(world->forge().str(t->second, false)); + types.insert(type); + if (world->uris().ingen_Graph == type) { + // Add lv2:Plugin as a type for graphs so plugin properties show up + types.insert(world->uris().lv2_Plugin); + } + } else { + world->log().error(fmt("<%1%> has non-URI type\n") % model->uri()); + } + } + + // Add every superclass of every type, recursively + RDFS::classes(world, types, true); + + return types; +} + +URISet +properties(World* world, SPtr<const Client::ObjectModel> model) +{ + URISet properties; + URISet types = RDFS::types(world, model); + + LilvNode* rdf_type = lilv_new_uri(world->lilv_world(), + LILV_NS_RDF "type"); + LilvNode* rdf_Property = lilv_new_uri(world->lilv_world(), + LILV_NS_RDF "Property"); + LilvNode* rdfs_domain = lilv_new_uri(world->lilv_world(), + LILV_NS_RDFS "domain"); + + LilvNodes* props = lilv_world_find_nodes( + world->lilv_world(), nullptr, rdf_type, rdf_Property); + LILV_FOREACH(nodes, p, props) { + const LilvNode* prop = lilv_nodes_get(props, p); + if (lilv_node_is_uri(prop)) { + LilvNodes* domains = lilv_world_find_nodes( + world->lilv_world(), prop, rdfs_domain, nullptr); + unsigned n_matching_domains = 0; + LILV_FOREACH(nodes, d, domains) { + const LilvNode* domain_node = lilv_nodes_get(domains, d); + if (!lilv_node_is_uri(domain_node)) { + // TODO: Blank node domains (e.g. unions) + continue; + } + + const URI domain(lilv_node_as_uri(domain_node)); + if (types.count(domain)) { + ++n_matching_domains; + } + } + + if (lilv_nodes_size(domains) == 0 || ( + n_matching_domains > 0 && + n_matching_domains == lilv_nodes_size(domains))) { + properties.insert(URI(lilv_node_as_uri(prop))); + } + + lilv_nodes_free(domains); + } + } + + lilv_node_free(rdfs_domain); + lilv_node_free(rdf_Property); + lilv_node_free(rdf_type); + + return properties; +} + +Objects +instances(World* world, const URISet& types) +{ + LilvNode* rdf_type = lilv_new_uri( + world->lilv_world(), LILV_NS_RDF "type"); + + Objects result; + for (const auto& t : types) { + LilvNode* type = lilv_new_uri(world->lilv_world(), t.c_str()); + LilvNodes* objects = lilv_world_find_nodes( + world->lilv_world(), nullptr, rdf_type, type); + LILV_FOREACH(nodes, o, objects) { + const LilvNode* object = lilv_nodes_get(objects, o); + if (!lilv_node_is_uri(object)) { + continue; + } + const std::string label = RDFS::label(world, object); + result.emplace(label, URI(lilv_node_as_string(object))); + } + lilv_node_free(type); + } + + lilv_node_free(rdf_type); + return result; +} + +URISet +range(World* world, const LilvNode* prop, bool recursive) +{ + LilvNode* rdfs_range = lilv_new_uri( + world->lilv_world(), LILV_NS_RDFS "range"); + + LilvNodes* nodes = lilv_world_find_nodes( + world->lilv_world(), prop, rdfs_range, nullptr); + + URISet ranges; + LILV_FOREACH(nodes, n, nodes) { + ranges.insert(URI(lilv_node_as_string(lilv_nodes_get(nodes, n)))); + } + + if (recursive) { + RDFS::classes(world, ranges, false); + } + + lilv_nodes_free(nodes); + lilv_node_free(rdfs_range); + return ranges; +} + +bool +is_a(World* world, const LilvNode* inst, const LilvNode* klass) +{ + LilvNode* rdf_type = lilv_new_uri(world->lilv_world(), LILV_NS_RDF "type"); + + const bool is_instance = lilv_world_ask( + world->lilv_world(), inst, rdf_type, klass); + + lilv_node_free(rdf_type); + return is_instance; +} + +} // namespace RDFS +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/RDFS.hpp b/src/gui/RDFS.hpp new file mode 100644 index 00000000..f59bbdf5 --- /dev/null +++ b/src/gui/RDFS.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_RDF_HPP +#define INGEN_GUI_RDF_HPP + +#include <map> +#include <set> +#include <string> + +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +namespace Ingen { + +class World; + +namespace Client { class ObjectModel; } + +namespace GUI { + +namespace RDFS { + +/** Set of URIs. */ +typedef std::set<URI> URISet; + +/** Label => Resource map. */ +typedef std::map<std::string, URI> Objects; + +/** Return the label of `node`. */ +std::string label(World* world, const LilvNode* node); + +/** Return the comment of `node`. */ +std::string comment(World* world, const LilvNode* node); + +/** Set `types` to its super/sub class closure. + * @param super If true, find all superclasses, otherwise all subclasses + */ +void classes(World* world, URISet& types, bool super); + +/** Set `types` to its super/sub datatype closure. + * @param super If true, find all supertypes, otherwise all subtypes. + */ +void datatypes(World* world, URISet& types, bool super); + +/** Get all instances of any class in `types`. */ +Objects instances(World* world, const URISet& types); + +/** Get all the types which `model` is an instance of. */ +URISet types(World* world, SPtr<const Client::ObjectModel> model); + +/** Get all the properties with domains appropriate for `model`. */ +URISet properties(World* world, SPtr<const Client::ObjectModel> model); + +/** Return the range (value types) of `prop`. + * @param recursive If true, include all subclasses. + */ +URISet range(World* world, const LilvNode* prop, bool recursive); + +/** Return true iff `inst` is-a `klass`. */ +bool is_a(World* world, const LilvNode* inst, const LilvNode* klass); + +} // namespace RDFS +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_RDF_HPP diff --git a/src/gui/RenameWindow.cpp b/src/gui/RenameWindow.cpp new file mode 100644 index 00000000..c83143d9 --- /dev/null +++ b/src/gui/RenameWindow.cpp @@ -0,0 +1,137 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> + +#include "ingen/Forge.hpp" +#include "ingen/Interface.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/ObjectModel.hpp" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +#include "App.hpp" +#include "RenameWindow.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +RenameWindow::RenameWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml) + : Window(cobject) +{ + xml->get_widget("rename_symbol_entry", _symbol_entry); + xml->get_widget("rename_label_entry", _label_entry); + xml->get_widget("rename_message_label", _message_label); + xml->get_widget("rename_cancel_button", _cancel_button); + xml->get_widget("rename_ok_button", _ok_button); + + _symbol_entry->signal_changed().connect( + sigc::mem_fun(this, &RenameWindow::values_changed)); + _label_entry->signal_changed().connect( + sigc::mem_fun(this, &RenameWindow::values_changed)); + _cancel_button->signal_clicked().connect( + sigc::mem_fun(this, &RenameWindow::cancel_clicked)); + _ok_button->signal_clicked().connect( + sigc::mem_fun(this, &RenameWindow::ok_clicked)); + + _ok_button->property_sensitive() = false; +} + +/** Set the object this window is renaming. + * This function MUST be called before using this object in any way. + */ +void +RenameWindow::set_object(SPtr<const ObjectModel> object) +{ + _object = object; + _symbol_entry->set_text(object->path().symbol()); + const Atom& name_atom = object->get_property(_app->uris().lv2_name); + _label_entry->set_text( + (name_atom.type() == _app->forge().String) ? name_atom.ptr<char>() : ""); +} + +void +RenameWindow::present(SPtr<const ObjectModel> object) +{ + set_object(object); + _symbol_entry->grab_focus(); + Gtk::Window::present(); +} + +void +RenameWindow::values_changed() +{ + const std::string& symbol = _symbol_entry->get_text(); + if (!Raul::Symbol::is_valid(symbol)) { + _message_label->set_text("Invalid symbol"); + _ok_button->property_sensitive() = false; + } else if (_object->symbol() != symbol && + _app->store()->object( + _object->parent()->path().child(Raul::Symbol(symbol)))) { + _message_label->set_text("An object already exists with that path"); + _ok_button->property_sensitive() = false; + } else { + _message_label->set_text(""); + _ok_button->property_sensitive() = true; + } +} + +void +RenameWindow::cancel_clicked() +{ + _symbol_entry->set_text(""); + hide(); +} + +/** Rename the object. + * + * It shouldn't be possible for this to be called with an invalid name set + * (since the Rename button should be deactivated). This is just shinification + * though - the engine will handle invalid names gracefully. + */ +void +RenameWindow::ok_clicked() +{ + const URIs& uris = _app->uris(); + const std::string& symbol_str = _symbol_entry->get_text(); + const std::string& label = _label_entry->get_text(); + Raul::Path path = _object->path(); + const Atom& name_atom = _object->get_property(uris.lv2_name); + + if (!label.empty() && (name_atom.type() != uris.forge.String || + label != name_atom.ptr<char>())) { + _app->set_property(path_to_uri(path), + uris.lv2_name, + _app->forge().alloc(label)); + } + + if (Raul::Symbol::is_valid(symbol_str)) { + const Raul::Symbol symbol(symbol_str); + if (symbol != _object->symbol()) { + path = _object->path().parent().child(symbol); + _app->interface()->move(_object->path(), path); + } + } + + hide(); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/RenameWindow.hpp b/src/gui/RenameWindow.hpp new file mode 100644 index 00000000..36264879 --- /dev/null +++ b/src/gui/RenameWindow.hpp @@ -0,0 +1,64 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_RENAMEWINDOW_HPP +#define INGEN_GUI_RENAMEWINDOW_HPP + +#include <gtkmm/builder.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/label.h> + +#include "ingen/client/ObjectModel.hpp" +#include "ingen/types.hpp" + +#include "Window.hpp" + +namespace Ingen { +namespace GUI { + +/** Rename window. Handles renaming of any (Ingen) object. + * + * \ingroup GUI + */ +class RenameWindow : public Window +{ +public: + RenameWindow(BaseObjectType* cobject, + const Glib::RefPtr<Gtk::Builder>& xml); + + void present(SPtr<const Client::ObjectModel> object); + +private: + void set_object(SPtr<const Client::ObjectModel> object); + + void values_changed(); + void cancel_clicked(); + void ok_clicked(); + + SPtr<const Client::ObjectModel> _object; + + Gtk::Entry* _symbol_entry; + Gtk::Entry* _label_entry; + Gtk::Label* _message_label; + Gtk::Button* _cancel_button; + Gtk::Button* _ok_button; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_RENAMEWINDOW_HPP diff --git a/src/gui/Style.cpp b/src/gui/Style.cpp new file mode 100644 index 00000000..81e6fb6c --- /dev/null +++ b/src/gui/Style.cpp @@ -0,0 +1,106 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cstdlib> +#include <fstream> +#include <map> +#include <string> + +#include "ganv/Port.hpp" +#include "ingen/Log.hpp" +#include "ingen/Parser.hpp" +#include "ingen/client/PluginModel.hpp" +#include "ingen/client/PortModel.hpp" + +#include "App.hpp" +#include "Style.hpp" +#include "Port.hpp" + +namespace Ingen { +namespace GUI { + +using namespace Ingen::Client; + +Style::Style(App& app) + // Colours from the Tango palette with modified V + : _app(app) +#ifdef INGEN_USE_LIGHT_THEME + , _audio_port_color(0xC8E6ABFF) // Green + , _control_port_color(0xAAC0E6FF) // Blue + , _cv_port_color(0xACE6E0FF) // Teal (between audio and control) + , _event_port_color(0xE6ABABFF) // Red + , _string_port_color(0xD8ABE6FF) // Plum +#else + , _audio_port_color(0x4A8A0EFF) // Green + , _control_port_color(0x244678FF) // Blue + , _cv_port_color(0x248780FF) // Teal (between audio and control) + , _event_port_color(0x960909FF) // Red + , _string_port_color(0x5C3566FF) // Plum +#endif +{ +} + +/** Loads settings from the rc file. Passing no parameter will load from + * the default location. + */ +void +Style::load_settings(std::string filename) +{ + /* ... */ +} + +/** Saves settings to rc file. Passing no parameter will save to the + * default location. + */ +void +Style::save_settings(std::string filename) +{ + /* ... */ +} + +/** Applies the current loaded settings to whichever parts of the app + * need updating. + */ +void +Style::apply_settings() +{ + /* ... */ +} + +uint32_t +Style::get_port_color(const Client::PortModel* p) +{ + const URIs& uris = _app.uris(); + if (p->is_a(uris.lv2_AudioPort)) { + return _audio_port_color; + } else if (p->is_a(uris.lv2_ControlPort)) { + return _control_port_color; + } else if (p->is_a(uris.lv2_CVPort)) { + return _cv_port_color; + } else if (p->supports(uris.atom_String)) { + return _string_port_color; + } else if (_app.can_control(p)) { + return _control_port_color; + } else if (p->is_a(uris.atom_AtomPort)) { + return _event_port_color; + } + + return 0x555555FF; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/Style.hpp b/src/gui/Style.hpp new file mode 100644 index 00000000..8e628a3d --- /dev/null +++ b/src/gui/Style.hpp @@ -0,0 +1,56 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_STYLE_HPP +#define INGEN_GUI_STYLE_HPP + +#include <cstdint> +#include <string> + +namespace Ingen { namespace Client { class PortModel; } } + +namespace Ingen { +namespace GUI { + +class App; +class Port; + +class Style +{ +public: + explicit Style(App& app); + + void load_settings(std::string filename = ""); + void save_settings(std::string filename = ""); + + void apply_settings(); + + uint32_t get_port_color(const Client::PortModel* p); + +private: + App& _app; + + uint32_t _audio_port_color; + uint32_t _control_port_color; + uint32_t _cv_port_color; + uint32_t _event_port_color; + uint32_t _string_port_color; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_STYLE_HPP diff --git a/src/gui/SubgraphModule.cpp b/src/gui/SubgraphModule.cpp new file mode 100644 index 00000000..6bbcf534 --- /dev/null +++ b/src/gui/SubgraphModule.cpp @@ -0,0 +1,102 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <utility> + +#include "ingen/Interface.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "NodeModule.hpp" +#include "GraphCanvas.hpp" +#include "GraphWindow.hpp" +#include "Port.hpp" +#include "SubgraphModule.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +SubgraphModule::SubgraphModule(GraphCanvas& canvas, + SPtr<const GraphModel> graph) + : NodeModule(canvas, graph) + , _graph(graph) +{ + assert(graph); +} + +bool +SubgraphModule::on_double_click(GdkEventButton* event) +{ + assert(_graph); + + SPtr<GraphModel> parent = dynamic_ptr_cast<GraphModel>(_graph->parent()); + + GraphWindow* const preferred = ( (parent && (event->state & GDK_SHIFT_MASK)) + ? nullptr + : app().window_factory()->graph_window(parent) ); + + app().window_factory()->present_graph(_graph, preferred); + return true; +} + +void +SubgraphModule::store_location(double ax, double ay) +{ + const URIs& uris = app().uris(); + + const Atom x(app().forge().make(static_cast<float>(ax))); + const Atom y(app().forge().make(static_cast<float>(ay))); + + if (x != _block->get_property(uris.ingen_canvasX) || + y != _block->get_property(uris.ingen_canvasY)) + { + app().interface()->put(_graph->uri(), + {{uris.ingen_canvasX, x}, + {uris.ingen_canvasY, y}}, + Resource::Graph::EXTERNAL); + } +} + +/** Browse to this graph in current (parent's) window + * (unless an existing window is displaying it) + */ +void +SubgraphModule::browse_to_graph() +{ + assert(_graph->parent()); + + SPtr<GraphModel> parent = dynamic_ptr_cast<GraphModel>(_graph->parent()); + + GraphWindow* const preferred = (parent) + ? app().window_factory()->graph_window(parent) + : nullptr; + + app().window_factory()->present_graph(_graph, preferred); +} + +void +SubgraphModule::menu_remove() +{ + app().interface()->del(_graph->uri()); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/SubgraphModule.hpp b/src/gui/SubgraphModule.hpp new file mode 100644 index 00000000..1b8df2fa --- /dev/null +++ b/src/gui/SubgraphModule.hpp @@ -0,0 +1,64 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_SUBGRAPHMODULE_HPP +#define INGEN_GUI_SUBGRAPHMODULE_HPP + +#include "ingen/types.hpp" + +#include "NodeModule.hpp" +#include "GraphPortModule.hpp" + +namespace Ingen { namespace Client { +class GraphModel; +class GraphWindow; +class PortModel; +} } + +namespace Ingen { +namespace GUI { + +class GraphCanvas; + +/** A module to represent a subgraph + * + * \ingroup GUI + */ +class SubgraphModule : public NodeModule +{ +public: + SubgraphModule(GraphCanvas& canvas, + SPtr<const Client::GraphModel> graph); + + virtual ~SubgraphModule() {} + + bool on_double_click(GdkEventButton* event); + + void store_location(double ax, double ay); + + void browse_to_graph(); + void menu_remove(); + + SPtr<const Client::GraphModel> graph() const { return _graph; } + +protected: + SPtr<const Client::GraphModel> _graph; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_SUBGRAPHMODULE_HPP diff --git a/src/gui/ThreadedLoader.cpp b/src/gui/ThreadedLoader.cpp new file mode 100644 index 00000000..7a80fa6e --- /dev/null +++ b/src/gui/ThreadedLoader.cpp @@ -0,0 +1,148 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <string> + +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/World.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "ThreadedLoader.hpp" + +using boost::optional; + +namespace Ingen { +namespace GUI { + +ThreadedLoader::ThreadedLoader(App& app, SPtr<Interface> engine) + : _app(app) + , _sem(0) + , _engine(std::move(engine)) + , _exit_flag(false) + , _thread(&ThreadedLoader::run, this) +{ + if (!parser()) { + app.log().warn("Parser unavailable, graph loading disabled\n"); + } +} + +ThreadedLoader::~ThreadedLoader() +{ + _exit_flag = true; + _sem.post(); + if (_thread.joinable()) { + _thread.join(); + } +} + +SPtr<Parser> +ThreadedLoader::parser() +{ + return _app.world()->parser(); +} + +void +ThreadedLoader::run() +{ + while (_sem.wait() && !_exit_flag) { + std::lock_guard<std::mutex> lock(_mutex); + while (!_events.empty()) { + _events.front()(); + _events.pop_front(); + } + } +} + +void +ThreadedLoader::load_graph(bool merge, + const FilePath& file_path, + optional<Raul::Path> engine_parent, + optional<Raul::Symbol> engine_symbol, + optional<Properties> engine_data) +{ + std::lock_guard<std::mutex> lock(_mutex); + + Glib::ustring engine_base = ""; + if (engine_parent) { + if (merge) { + engine_base = engine_parent.get(); + } else { + engine_base = engine_parent.get().base(); + } + } + + _events.push_back(sigc::hide_return( + sigc::bind(sigc::mem_fun(this, &ThreadedLoader::load_graph_event), + file_path, + engine_parent, + engine_symbol, + engine_data))); + + _sem.post(); +} + +void +ThreadedLoader::load_graph_event(const FilePath& file_path, + optional<Raul::Path> engine_parent, + optional<Raul::Symbol> engine_symbol, + optional<Properties> engine_data) +{ + std::lock_guard<std::mutex> lock(_app.world()->rdf_mutex()); + + _app.world()->parser()->parse_file(_app.world(), + _app.world()->interface().get(), + file_path, + engine_parent, + engine_symbol, + engine_data); +} + +void +ThreadedLoader::save_graph(SPtr<const Client::GraphModel> model, const URI& uri) +{ + std::lock_guard<std::mutex> lock(_mutex); + + _events.push_back(sigc::hide_return( + sigc::bind(sigc::mem_fun(this, &ThreadedLoader::save_graph_event), + model, + uri))); + + _sem.post(); +} + +void +ThreadedLoader::save_graph_event(SPtr<const Client::GraphModel> model, + const URI& uri) +{ + assert(uri.scheme() == "file"); + if (_app.serialiser()) { + std::lock_guard<std::mutex> lock(_app.world()->rdf_mutex()); + + if (uri.string().find(".ingen") != std::string::npos) { + _app.serialiser()->write_bundle(model, uri); + } else { + _app.serialiser()->start_to_file(model->path(), std::string(uri.path())); + _app.serialiser()->serialise(model); + _app.serialiser()->finish(); + } + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/ThreadedLoader.hpp b/src/gui/ThreadedLoader.hpp new file mode 100644 index 00000000..79ef6466 --- /dev/null +++ b/src/gui/ThreadedLoader.hpp @@ -0,0 +1,96 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_THREADEDLOADER_HPP +#define INGEN_GUI_THREADEDLOADER_HPP + +#include <thread> + +#include <cassert> +#include <list> +#include <mutex> +#include <string> + +#include <boost/optional.hpp> + +#include "ingen/FilePath.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Parser.hpp" +#include "ingen/Serialiser.hpp" +#include "raul/Semaphore.hpp" + +namespace Ingen { + +class URI; + +namespace GUI { + +/** Thread for loading graph files. + * + * This is a seperate thread so it can send all the loading message without + * blocking everything else, so the app can respond to the incoming events + * caused as a result of the graph loading, while the graph loads. + * + * Implemented as a slave with a list of closures (events) which processes + * all events in the (mutex protected) list each time it's whipped. + * + * \ingroup GUI + */ +class ThreadedLoader +{ +public: + ThreadedLoader(App& app, + SPtr<Interface> engine); + + ~ThreadedLoader(); + + void load_graph(bool merge, + const FilePath& file_path, + boost::optional<Raul::Path> engine_parent, + boost::optional<Raul::Symbol> engine_symbol, + boost::optional<Properties> engine_data); + + void save_graph(SPtr<const Client::GraphModel> model, const URI& uri); + + SPtr<Parser> parser(); + +private: + void load_graph_event(const FilePath& file_path, + boost::optional<Raul::Path> engine_parent, + boost::optional<Raul::Symbol> engine_symbol, + boost::optional<Properties> engine_data); + + void save_graph_event(SPtr<const Client::GraphModel> model, + const URI& filename); + + /** Returns nothing and takes no parameters (because they have all been bound) */ + typedef sigc::slot<void> Closure; + + void run(); + + App& _app; + Raul::Semaphore _sem; + SPtr<Interface> _engine; + std::mutex _mutex; + std::list<Closure> _events; + bool _exit_flag; + std::thread _thread; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_LOADERRTHREAD_HPP diff --git a/src/gui/URIEntry.cpp b/src/gui/URIEntry.cpp new file mode 100644 index 00000000..0b81afd7 --- /dev/null +++ b/src/gui/URIEntry.cpp @@ -0,0 +1,192 @@ +/* + This file is part of Ingen. + Copyright 2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <unordered_map> + +#include "App.hpp" +#include "RDFS.hpp" +#include "URIEntry.hpp" + +namespace Ingen { +namespace GUI { + +URIEntry::URIEntry(App* app, std::set<URI> types, const std::string& value) + : Gtk::HBox(false, 4) + , _app(app) + , _types(std::move(types)) + , _menu_button(Gtk::manage(new Gtk::Button("≡"))) + , _entry(Gtk::manage(new Gtk::Entry())) +{ + pack_start(*_entry, true, true); + pack_start(*_menu_button, false, true); + + _entry->set_text(value); + + _menu_button->signal_event().connect( + sigc::mem_fun(this, &URIEntry::menu_button_event)); +} + +Gtk::Menu* +URIEntry::build_value_menu() +{ + World* world = _app->world(); + LilvWorld* lworld = world->lilv_world(); + Gtk::Menu* menu = new Gtk::Menu(); + + LilvNode* owl_onDatatype = lilv_new_uri(lworld, LILV_NS_OWL "onDatatype"); + LilvNode* rdf_type = lilv_new_uri(lworld, LILV_NS_RDF "type"); + LilvNode* rdfs_Class = lilv_new_uri(lworld, LILV_NS_RDFS "Class"); + LilvNode* rdfs_Datatype = lilv_new_uri(lworld, LILV_NS_RDFS "Datatype"); + LilvNode* rdfs_subClassOf = lilv_new_uri(lworld, LILV_NS_RDFS "subClassOf"); + + RDFS::Objects values = RDFS::instances(world, _types); + + for (const auto& v : values) { + const LilvNode* inst = lilv_new_uri(lworld, v.second.c_str()); + std::string label = v.first; + if (label.empty()) { + // No label, show raw URI + label = lilv_node_as_string(inst); + } + + if (lilv_world_ask(world->lilv_world(), inst, rdf_type, rdfs_Class) || + lilv_world_ask(world->lilv_world(), inst, rdf_type, rdfs_Datatype)) { + // This value is a class or datatype... + if (!lilv_world_ask(lworld, inst, rdfs_subClassOf, nullptr) && + !lilv_world_ask(lworld, inst, owl_onDatatype, nullptr)) { + // ... which is not a subtype of another, add menu + add_class_menu_item(menu, inst, label); + } + } else { + // Value is not a class, add item + menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + std::string("_") + label, + sigc::bind(sigc::mem_fun(this, &URIEntry::uri_chosen), + std::string(lilv_node_as_uri(inst))))); + _app->set_tooltip(&menu->items().back(), inst); + } + } + + lilv_node_free(owl_onDatatype); + lilv_node_free(rdf_type); + lilv_node_free(rdfs_Class); + lilv_node_free(rdfs_Datatype); + lilv_node_free(rdfs_subClassOf); + + return menu; +} + +Gtk::Menu* +URIEntry::build_subclass_menu(const LilvNode* klass) +{ + World* world = _app->world(); + LilvWorld* lworld = world->lilv_world(); + + LilvNode* owl_onDatatype = lilv_new_uri(lworld, LILV_NS_OWL "onDatatype"); + LilvNode* rdfs_subClassOf = lilv_new_uri(lworld, LILV_NS_RDFS "subClassOf"); + + LilvNodes* subclasses = lilv_world_find_nodes( + lworld, nullptr, rdfs_subClassOf, klass); + LilvNodes* subtypes = lilv_world_find_nodes( + lworld, nullptr, owl_onDatatype, klass); + + if (lilv_nodes_size(subclasses) == 0 && lilv_nodes_size(subtypes) == 0) { + return nullptr; + } + + Gtk::Menu* menu = new Gtk::Menu(); + + // Add "header" item for choosing this class itself + add_leaf_menu_item(menu, klass, RDFS::label(world, klass)); + menu->items().push_back(Gtk::Menu_Helpers::SeparatorElem()); + + // Put subclasses/types in a map keyed by label (to sort menu) + std::map<std::string, const LilvNode*> entries; + LILV_FOREACH(nodes, s, subclasses) { + const LilvNode* node = lilv_nodes_get(subclasses, s); + entries.emplace(RDFS::label(world, node), node); + } + LILV_FOREACH(nodes, s, subtypes) { + const LilvNode* node = lilv_nodes_get(subtypes, s); + entries.emplace(RDFS::label(world, node), node); + } + + // Add an item (possibly with a submenu) for each subclass/type + for (const auto& e : entries) { + add_class_menu_item(menu, e.second, e.first); + } + + lilv_nodes_free(subtypes); + lilv_nodes_free(subclasses); + lilv_node_free(rdfs_subClassOf); + lilv_node_free(owl_onDatatype); + + return menu; +} + +void +URIEntry::add_leaf_menu_item(Gtk::Menu* menu, + const LilvNode* node, + const std::string& label) +{ + menu->items().push_back( + Gtk::Menu_Helpers::MenuElem( + std::string("_") + label, + sigc::bind(sigc::mem_fun(this, &URIEntry::uri_chosen), + std::string(lilv_node_as_uri(node))))); + + _app->set_tooltip(&menu->items().back(), node); +} + +void +URIEntry::add_class_menu_item(Gtk::Menu* menu, + const LilvNode* klass, + const std::string& label) +{ + Gtk::Menu* submenu = build_subclass_menu(klass); + + if (submenu) { + menu->items().push_back(Gtk::Menu_Helpers::MenuElem(label)); + menu->items().back().set_submenu(*Gtk::manage(submenu)); + } else { + add_leaf_menu_item(menu, klass, label); + } + + _app->set_tooltip(&menu->items().back(), klass); +} + +void +URIEntry::uri_chosen(const std::string& uri) +{ + _entry->set_text(uri); +} + +bool +URIEntry::menu_button_event(GdkEvent* ev) +{ + if (ev->type != GDK_BUTTON_PRESS) { + return false; + } + + Gtk::Menu* menu = Gtk::manage(build_value_menu()); + menu->popup(ev->button.button, ev->button.time); + + return true; +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/URIEntry.hpp b/src/gui/URIEntry.hpp new file mode 100644 index 00000000..2f55a3d9 --- /dev/null +++ b/src/gui/URIEntry.hpp @@ -0,0 +1,68 @@ +/* + This file is part of Ingen. + Copyright 2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_URI_ENTRY_HPP +#define INGEN_GUI_URI_ENTRY_HPP + +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/entry.h> +#include <gtkmm/menu.h> + +#include "lilv/lilv.h" + +namespace Ingen { +namespace GUI { + +class App; + +class URIEntry : public Gtk::HBox { +public: + /** Create a widget for entering URIs. + * + * If `types` is given, then a menu button will be shown which pops up a + * enu for easily choosing known values with valid types. + */ + URIEntry(App* app, std::set<URI> types, const std::string& value); + + std::string get_text() { return _entry->get_text(); } + Glib::SignalProxy0<void> signal_changed() { return _entry->signal_changed(); } + +private: + Gtk::Menu* build_value_menu(); + Gtk::Menu* build_subclass_menu(const LilvNode* klass); + + void add_leaf_menu_item(Gtk::Menu* menu, + const LilvNode* node, + const std::string& label); + + void add_class_menu_item(Gtk::Menu* menu, + const LilvNode* klass, + const std::string& label); + + void uri_chosen(const std::string& uri); + bool menu_button_event(GdkEvent* ev); + + App* _app; + const std::set<URI> _types; + Gtk::Button* _menu_button; + Gtk::Entry* _entry; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_URI_ENTRY_HPP diff --git a/src/gui/WidgetFactory.cpp b/src/gui/WidgetFactory.cpp new file mode 100644 index 00000000..afb6a07f --- /dev/null +++ b/src/gui/WidgetFactory.cpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <fstream> +#include <string> + +#include "ingen/Log.hpp" +#include "ingen/runtime_paths.hpp" + +#include "WidgetFactory.hpp" + +namespace Ingen { +namespace GUI { + +Glib::ustring WidgetFactory::ui_filename = ""; + +inline static bool +is_readable(const std::string& filename) +{ + std::ifstream fs(filename.c_str()); + const bool fail = fs.fail(); + fs.close(); + return !fail; +} + +void +WidgetFactory::find_ui_file() +{ + // Try file in bundle (directory where executable resides) + ui_filename = Ingen::bundle_file_path("ingen_gui.ui"); + if (is_readable(ui_filename)) { + return; + } + + // Try ENGINE_UI_PATH from the environment + const char* const env_path = getenv("INGEN_UI_PATH"); + if (env_path && is_readable(env_path)) { + ui_filename = env_path; + return; + } + + // Try the default system installed path + ui_filename = Ingen::data_file_path("ingen_gui.ui"); + if (is_readable(ui_filename)) { + return; + } + + throw std::runtime_error((fmt("Unable to find ingen_gui.ui in %1%\n") + % INGEN_DATA_DIR).str()); +} + +Glib::RefPtr<Gtk::Builder> +WidgetFactory::create(const std::string& toplevel_widget) +{ + if (ui_filename.empty()) { + find_ui_file(); + } + + if (toplevel_widget.empty()) { + return Gtk::Builder::create_from_file(ui_filename); + } else { + return Gtk::Builder::create_from_file(ui_filename, toplevel_widget.c_str()); + } +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/WidgetFactory.hpp b/src/gui/WidgetFactory.hpp new file mode 100644 index 00000000..92f4dffe --- /dev/null +++ b/src/gui/WidgetFactory.hpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_GLADEFACTORY_HPP +#define INGEN_GUI_GLADEFACTORY_HPP + +#include <string> + +#include <glibmm.h> +#include <gtkmm/builder.h> + +namespace Ingen { +namespace GUI { + +/** Loads widgets from an XML description. + * Purely static. + * + * \ingroup GUI + */ +class WidgetFactory { +public: + static Glib::RefPtr<Gtk::Builder> + create(const std::string& toplevel_widget=""); + + template<typename T> + static void get_widget(const Glib::ustring& name, T*& widget) { + Glib::RefPtr<Gtk::Builder> xml = create(name); + xml->get_widget(name, widget); + } + + template<typename T> + static void get_widget_derived(const Glib::ustring& name, T*& widget) { + Glib::RefPtr<Gtk::Builder> xml = create(name); + xml->get_widget_derived(name, widget); + } + +private: + static void find_ui_file(); + static Glib::ustring ui_filename; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_GLADEFACTORY_HPP diff --git a/src/gui/Window.hpp b/src/gui/Window.hpp new file mode 100644 index 00000000..2a5c9843 --- /dev/null +++ b/src/gui/Window.hpp @@ -0,0 +1,78 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_WINDOW_HPP +#define INGEN_GUI_WINDOW_HPP + +#include <gtkmm/dialog.h> +#include <gtkmm/window.h> + +namespace Ingen { + +namespace GUI { + +class App; + +/** Ingen GUI Window + * \ingroup GUI + */ +class Window : public Gtk::Window +{ +public: + Window() : Gtk::Window(), _app(nullptr) {} + explicit Window(BaseObjectType* cobject) : Gtk::Window(cobject), _app(nullptr) {} + + virtual void init_window(App& app) { _app = &app; } + + bool on_key_press_event(GdkEventKey* event) { + if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) { + hide(); + return true; + } + return Gtk::Window::on_key_press_event(event); + } + + static bool key_press_handler(Gtk::Window* win, GdkEventKey* event); + + App* _app; +}; + +/** Ingen GUI Dialog + * \ingroup GUI + */ +class Dialog : public Gtk::Dialog +{ +public: + Dialog() : Gtk::Dialog(), _app(nullptr) {} + explicit Dialog(BaseObjectType* cobject) : Gtk::Dialog(cobject), _app(nullptr) {} + + virtual void init_dialog(App& app) { _app = &app; } + + bool on_key_press_event(GdkEventKey* event) { + if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) { + hide(); + return true; + } + return Gtk::Window::on_key_press_event(event); + } + + App* _app; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_WINDOW_HPP diff --git a/src/gui/WindowFactory.cpp b/src/gui/WindowFactory.cpp new file mode 100644 index 00000000..5dbdbe98 --- /dev/null +++ b/src/gui/WindowFactory.cpp @@ -0,0 +1,302 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <stdexcept> +#include <string> + +#include "ingen/Log.hpp" +#include "ingen/client/GraphModel.hpp" + +#include "App.hpp" +#include "LoadGraphWindow.hpp" +#include "LoadPluginWindow.hpp" +#include "NewSubgraphWindow.hpp" +#include "GraphView.hpp" +#include "GraphWindow.hpp" +#include "PropertiesWindow.hpp" +#include "RenameWindow.hpp" +#include "WidgetFactory.hpp" +#include "WindowFactory.hpp" + +namespace Ingen { + +using namespace Client; + +namespace GUI { + +WindowFactory::WindowFactory(App& app) + : _app(app) + , _main_box(nullptr) + , _load_plugin_win(nullptr) + , _load_graph_win(nullptr) + , _new_subgraph_win(nullptr) + , _properties_win(nullptr) +{ + WidgetFactory::get_widget_derived("load_plugin_win", _load_plugin_win); + WidgetFactory::get_widget_derived("load_graph_win", _load_graph_win); + WidgetFactory::get_widget_derived("new_subgraph_win", _new_subgraph_win); + WidgetFactory::get_widget_derived("properties_win", _properties_win); + WidgetFactory::get_widget_derived("rename_win", _rename_win); + + if (!(_load_plugin_win && _load_graph_win && _new_subgraph_win + && _properties_win && _rename_win)) { + throw std::runtime_error("failed to load window widgets\n"); + } + + _load_plugin_win->init_window(app); + _load_graph_win->init(app); + _new_subgraph_win->init_window(app); + _properties_win->init_window(app); + _rename_win->init_window(app); +} + +WindowFactory::~WindowFactory() +{ + for (const auto& w : _graph_windows) { + delete w.second; + } +} + +void +WindowFactory::clear() +{ + for (const auto& w : _graph_windows) { + delete w.second; + } + + _graph_windows.clear(); +} + +/** Returns the number of Graph windows currently visible. + */ +size_t +WindowFactory::num_open_graph_windows() +{ + size_t ret = 0; + for (const auto& w : _graph_windows) { + if (w.second->is_visible()) { + ++ret; + } + } + + return ret; +} + +GraphBox* +WindowFactory::graph_box(SPtr<const GraphModel> graph) +{ + GraphWindow* window = graph_window(graph); + if (window) { + return window->box(); + } else { + return _main_box; + } +} + +GraphWindow* +WindowFactory::graph_window(SPtr<const GraphModel> graph) +{ + if (!graph) { + return nullptr; + } + + auto w = _graph_windows.find(graph->path()); + + return (w == _graph_windows.end()) ? nullptr : w->second; +} + +GraphWindow* +WindowFactory::parent_graph_window(SPtr<const BlockModel> block) +{ + if (!block) { + return nullptr; + } + + return graph_window(dynamic_ptr_cast<GraphModel>(block->parent())); +} + +/** Present a GraphWindow for a Graph. + * + * If `preferred` is not NULL, it will be set to display `graph` if the graph + * does not already have a visible window, otherwise that window will be + * presented and `preferred` left unmodified. + */ +void +WindowFactory::present_graph(SPtr<const GraphModel> graph, + GraphWindow* preferred, + SPtr<GraphView> view) +{ + assert(!view || view->graph() == graph); + + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + (*w).second->present(); + } else if (preferred) { + w = _graph_windows.find(preferred->graph()->path()); + assert((*w).second == preferred); + + preferred->box()->set_graph(graph, view); + _graph_windows.erase(w); + _graph_windows[graph->path()] = preferred; + preferred->present(); + + } else { + GraphWindow* win = new_graph_window(graph, view); + win->present(); + } +} + +GraphWindow* +WindowFactory::new_graph_window(SPtr<const GraphModel> graph, + SPtr<GraphView> view) +{ + assert(!view || view->graph() == graph); + + GraphWindow* win = nullptr; + WidgetFactory::get_widget_derived("graph_win", win); + if (!win) { + _app.log().error("Failed to load graph window widget\n"); + return nullptr; + } + + win->init_window(_app); + + win->box()->set_graph(graph, view); + _graph_windows[graph->path()] = win; + + win->signal_delete_event().connect( + sigc::bind<0>(sigc::mem_fun(this, &WindowFactory::remove_graph_window), + win)); + + return win; +} + +bool +WindowFactory::remove_graph_window(GraphWindow* win, GdkEventAny* ignored) +{ + if (_graph_windows.size() <= 1) { + return !_app.quit(win); + } + + auto w = _graph_windows.find(win->graph()->path()); + + assert((*w).second == win); + _graph_windows.erase(w); + + delete win; + + return false; +} + +void +WindowFactory::present_load_plugin(SPtr<const GraphModel> graph, + Properties data) +{ + _app.request_plugins_if_necessary(); + + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + _load_plugin_win->set_transient_for(*w->second); + } + + _load_plugin_win->set_modal(false); + _load_plugin_win->set_type_hint(Gdk::WINDOW_TYPE_HINT_DIALOG); + if (w->second) { + int width, height; + w->second->get_size(width, height); + _load_plugin_win->set_default_size(width - width / 8, height / 2); + } + _load_plugin_win->set_title( + std::string("Load Plugin - ") + graph->path() + " - Ingen"); + _load_plugin_win->present(graph, data); +} + +void +WindowFactory::present_load_graph(SPtr<const GraphModel> graph, + Properties data) +{ + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + _load_graph_win->set_transient_for(*w->second); + } + + _load_graph_win->present(graph, true, data); +} + +void +WindowFactory::present_load_subgraph(SPtr<const GraphModel> graph, + Properties data) +{ + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + _load_graph_win->set_transient_for(*w->second); + } + + _load_graph_win->present(graph, false, data); +} + +void +WindowFactory::present_new_subgraph(SPtr<const GraphModel> graph, + Properties data) +{ + auto w = _graph_windows.find(graph->path()); + + if (w != _graph_windows.end()) { + _new_subgraph_win->set_transient_for(*w->second); + } + + _new_subgraph_win->present(graph, data); +} + +void +WindowFactory::present_rename(SPtr<const ObjectModel> object) +{ + auto w = _graph_windows.find(object->path()); + if (w == _graph_windows.end()) { + w = _graph_windows.find(object->path().parent()); + } + + if (w != _graph_windows.end()) { + _rename_win->set_transient_for(*w->second); + } + + _rename_win->present(object); +} + +void +WindowFactory::present_properties(SPtr<const ObjectModel> object) +{ + auto w = _graph_windows.find(object->path()); + if (w == _graph_windows.end()) { + w = _graph_windows.find(object->path().parent()); + } + if (w == _graph_windows.end()) { + w = _graph_windows.find(object->path().parent().parent()); + } + + if (w != _graph_windows.end()) { + _properties_win->set_transient_for(*w->second); + } + + _properties_win->present(object); +} + +} // namespace GUI +} // namespace Ingen diff --git a/src/gui/WindowFactory.hpp b/src/gui/WindowFactory.hpp new file mode 100644 index 00000000..ea8b909b --- /dev/null +++ b/src/gui/WindowFactory.hpp @@ -0,0 +1,99 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_WINDOWFACTORY_HPP +#define INGEN_GUI_WINDOWFACTORY_HPP + +#include <map> + +#include "ingen/Node.hpp" +#include "ingen/types.hpp" + +namespace Ingen { + +namespace Client { +class BlockModel; +class ObjectModel; +class GraphModel; +} + +namespace GUI { + +class App; +class GraphBox; +class GraphView; +class GraphWindow; +class LoadGraphWindow; +class LoadPluginWindow; +class NewSubgraphWindow; +class PropertiesWindow; +class RenameWindow; + +/** Manager/Factory for all windows. + * + * This serves as a nice centralized spot for all window management issues, + * as well as an enumeration of all windows (the goal being to reduce that + * number as much as possible). + */ +class WindowFactory { +public: + explicit WindowFactory(App& app); + ~WindowFactory(); + + size_t num_open_graph_windows(); + + GraphBox* graph_box(SPtr<const Client::GraphModel> graph); + GraphWindow* graph_window(SPtr<const Client::GraphModel> graph); + GraphWindow* parent_graph_window(SPtr<const Client::BlockModel> block); + + void present_graph( + SPtr<const Client::GraphModel> graph, + GraphWindow* preferred = NULL, + SPtr<GraphView> view = SPtr<GraphView>()); + + void present_load_plugin(SPtr<const Client::GraphModel> graph, Properties data=Properties()); + void present_load_graph(SPtr<const Client::GraphModel> graph, Properties data=Properties()); + void present_load_subgraph(SPtr<const Client::GraphModel> graph, Properties data=Properties()); + void present_new_subgraph(SPtr<const Client::GraphModel> graph, Properties data=Properties()); + void present_rename(SPtr<const Client::ObjectModel> object); + void present_properties(SPtr<const Client::ObjectModel> object); + + bool remove_graph_window(GraphWindow* win, GdkEventAny* ignored = NULL); + + void set_main_box(GraphBox* box) { _main_box = box; } + + void clear(); + +private: + typedef std::map<Raul::Path, GraphWindow*> GraphWindowMap; + + GraphWindow* new_graph_window(SPtr<const Client::GraphModel> graph, + SPtr<GraphView> view); + + App& _app; + GraphBox* _main_box; + GraphWindowMap _graph_windows; + LoadPluginWindow* _load_plugin_win; + LoadGraphWindow* _load_graph_win; + NewSubgraphWindow* _new_subgraph_win; + PropertiesWindow* _properties_win; + RenameWindow* _rename_win; +}; + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_WINDOWFACTORY_HPP diff --git a/src/gui/ingen_gui.cpp b/src/gui/ingen_gui.cpp new file mode 100644 index 00000000..677296fd --- /dev/null +++ b/src/gui/ingen_gui.cpp @@ -0,0 +1,67 @@ +/* + This file is part of Ingen. + Copyright 2007-2018 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Configuration.hpp" +#include "ingen/Module.hpp" +#include "ingen/QueuedInterface.hpp" +#include "ingen/client/SigClientInterface.hpp" + +#include "App.hpp" + +namespace Ingen { +namespace GUI { + +struct GUIModule : public Module { + using SigClientInterface = Client::SigClientInterface; + + void load(World* world) { + URI uri(world->conf().option("connect").ptr<char>()); + if (!world->interface()) { + world->set_interface( + world->new_interface(URI(uri), make_client(world))); + } else if (!dynamic_ptr_cast<SigClientInterface>( + world->interface()->respondee())) { + world->interface()->set_respondee(make_client(world)); + } + + app = GUI::App::create(world); + } + + void run(World* world) { + app->run(); + } + + SPtr<Interface> make_client(World* const world) { + SPtr<SigClientInterface> sci(new SigClientInterface()); + return world->engine() ? sci : SPtr<Interface>(new QueuedInterface(sci)); + } + + SPtr<GUI::App> app; +}; + +} // namespace GUI +} // namespace Ingen + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + Glib::thread_init(); + return new Ingen::GUI::GUIModule(); +} + +} // extern "C" diff --git a/src/gui/ingen_gui.gladep b/src/gui/ingen_gui.gladep new file mode 100644 index 00000000..184ff460 --- /dev/null +++ b/src/gui/ingen_gui.gladep @@ -0,0 +1,9 @@ +<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*--> +<!DOCTYPE glade-project SYSTEM "http://glade.gnome.org/glade-project-2.0.dtd"> + +<glade-project> + <name>Ingen</name> + <program_name>ingen</program_name> + <language>C++</language> + <gnome_support>FALSE</gnome_support> +</glade-project> diff --git a/src/gui/ingen_gui.ui b/src/gui/ingen_gui.ui new file mode 100644 index 00000000..9e751064 --- /dev/null +++ b/src/gui/ingen_gui.ui @@ -0,0 +1,3049 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.24"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkAboutDialog" id="about_win"> + <property name="can_focus">False</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">normal</property> + <property name="program_name">Ingen</property> + <property name="version">@INGEN_VERSION@</property> + <property name="copyright" translatable="yes">Copyright 2005-2015 David Robillard <http://drobilla.net></property> + <property name="website">http://drobilla.net/software/ingen</property> + <property name="license" translatable="yes">Licensed under the GNU Affero GPL, Version 3 or later. + +See COPYING file included with this distribution, or http://www.gnu.org/licenses/agpl.txt for more information</property> + <property name="authors">David Robillard <d@drobilla.net></property> + <property name="translator_credits" translatable="yes" comments="TRANSLATORS: Replace this string with your names, one name per line.">translator-credits</property> + <property name="artists">Usability / UI Design: + Thorsten Wilms</property> + <property name="wrap_license">True</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox3"> + <property name="can_focus">False</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area3"> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkMenu" id="canvas_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckMenuItem" id="canvas_menu_edit"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Edit</property> + <property name="use_underline">True</property> + <property name="active">True</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="input1"> + <property name="label">_Input</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <child type="submenu"> + <object class="GtkMenu" id="input1_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_audio_input"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Audio</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_audio_input_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_cv_input"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">C_V</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_cv_input_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_control_input"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Control</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_control_input_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_event_input"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Event</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_event_input_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="output1"> + <property name="label">_Output</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <child type="submenu"> + <object class="GtkMenu" id="output1_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_audio_output"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Audio</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_audio_output_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_cv_output"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">C_V</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_cv_output_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_control_output"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Control</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_control_output_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="canvas_menu_add_event_output"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Event</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_canvas_menu_add_event_output_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="canvas_menu_load_plugin"> + <property name="label">_Find Plugin...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_canvas_menu_add_plugin_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="canvas_menu_load_graph"> + <property name="label">_Load Graph...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_canvas_menu_load_graph_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="canvas_menu_new_graph"> + <property name="label">_New Graph...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_canvas_menu_new_graph_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="canvas_menu_properties"> + <property name="label">P_roperties...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <signal name="activate" handler="on_canvas_menu_properties_activate" swapped="no"/> + </object> + </child> + </object> + <object class="GtkWindow" id="config_win"> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Configuration - Ingen</property> + <child> + <object class="GtkVBox" id="vbox13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkTable" id="table9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <child> + <object class="GtkLabel" id="label90"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><b>Graph Search Path: </b></property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkEntry" id="config_path_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label91"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes"><i>Example: /foo/bar:/home/user/graphs</i></property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label103"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="config_save_button"> + <property name="label">gtk-save</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="config_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="config_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkDialog" id="connect_win"> + <property name="can_focus">False</property> + <property name="border_width">6</property> + <property name="title" translatable="yes">Engine - Ingen</property> + <property name="resizable">False</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="connect_quit_button"> + <property name="label">gtk-quit</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="connect_disconnect_button"> + <property name="label">gtk-disconnect</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="connect_connect_button"> + <property name="label">gtk-connect</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox19"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="hbox61"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="connect_icon"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xpad">12</property> + <property name="stock">gtk-disconnect</property> + <property name="icon-size">3</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="homogeneous">True</property> + <child> + <object class="GtkProgressBar" id="connect_progress_bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="pulse_step">0.10000000149</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="connect_progress_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Not connected</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">4</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table18"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">2</property> + <property name="row_spacing">8</property> + <child> + <object class="GtkHBox" id="hbox64"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkSpinButton" id="connect_port_spinbutton"> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">GTK_FILL</property> + <property name="x_padding">8</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox67"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkEntry" id="connect_url_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="activates_default">True</property> + <property name="width_chars">28</property> + <property name="text" translatable="yes">unix:///tmp/ingen.sock</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + <property name="x_padding">8</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="connect_server_radiobutton"> + <property name="label" translatable="yes">_Connect to engine at: </property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="connect_launch_radiobutton"> + <property name="label" translatable="yes">_Launch separate engine on port: </property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">connect_server_radiobutton</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="connect_internal_radiobutton"> + <property name="label" translatable="yes">Start local _JACK engine</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">connect_server_radiobutton</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label131"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="connect_deactivate_button"> + <property name="label" translatable="yes">D_eactivate</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="connect_activate_button"> + <property name="label" translatable="yes">_Activate</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">6</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="0">connect_quit_button</action-widget> + <action-widget response="-6">connect_disconnect_button</action-widget> + <action-widget response="-6">connect_connect_button</action-widget> + </action-widgets> + </object> + <object class="GtkWindow" id="graph_tree_win"> + <property name="width_request">320</property> + <property name="height_request">340</property> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Graphs - Ingen</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow8"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">3</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTreeView" id="graphs_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="rules_hint">True</property> + </object> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="graph_win"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Ingen</property> + <property name="default_width">776</property> + <property name="default_height">480</property> + <child> + <object class="GtkVBox" id="graph_win_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuBar" id="menubar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuItem" id="graph_file_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_File</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="graph_file_menu_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="graph_import_menuitem"> + <property name="label">_Import...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="I" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_import_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_save_menuitem"> + <property name="label">gtk-save</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="S" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_file_save_graph_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_save_as_menuitem"> + <property name="label">Save _As...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="S" signal="activate" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_save_as_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_export_image_menuitem"> + <property name="label">_Export Image...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="R" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_draw_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator11"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_close_menuitem"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="W" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_file_close_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_quit_menuitem"> + <property name="label">gtk-quit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="Q" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_file_quit_nokill_menuitem_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="edit2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Edit</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="edit2_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="graph_undo_menuitem"> + <property name="label">gtk-undo</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="Z" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_undo_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_redo_menuitem"> + <property name="label">gtk-redo</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="Z" signal="activate" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_redo_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_cut_menuitem"> + <property name="label">gtk-cut</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_graph_cut_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_copy_menuitem"> + <property name="label">gtk-copy</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="C" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_copy_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_paste_menuitem"> + <property name="label">gtk-paste</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="V" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_paste_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_delete_menuitem"> + <property name="label">gtk-delete</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="Delete" signal="activate"/> + <signal name="activate" handler="on_graph_delete_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_select_all_menuitem"> + <property name="label">gtk-select-all</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="A" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_select_all_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_arrange_menuitem"> + <property name="label">Arrange</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="G" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_view_control_window_menuitem"> + <property name="label">C_ontrols...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="O" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_view_control_window_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_properties_menuitem"> + <property name="label">gtk-properties</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="P" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_properties_menuitem_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="graph_graph_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_View</property> + <property name="use_underline">True</property> + <child type="submenu"> + <object class="GtkMenu" id="graph_graph_menu_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkCheckMenuItem" id="graph_animate_signals_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Update control ports as values change.</property> + <property name="label" translatable="yes">Animate Signa_ls</property> + <property name="use_underline">True</property> + <accelerator key="l" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_sprung_layout_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Sprung Layou_t</property> + <property name="use_underline">True</property> + <accelerator key="t" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_human_names_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Human names</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <accelerator key="H" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_show_port_names_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Port _Names</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <accelerator key="n" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_doc_pane_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Documentation Pane</property> + <property name="use_underline">True</property> + <accelerator key="D" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="graph_status_bar_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Status Bar</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <accelerator key="b" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_zoom_in_menuitem"> + <property name="label">gtk-zoom-in</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="equal" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_zoom_out_menuitem"> + <property name="label">gtk-zoom-out</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="minus" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_zoom_normal_menuitem"> + <property name="label">gtk-zoom-100</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="0" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_zoom_full_menuitem"> + <property name="label">gtk-zoom-fit</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="F" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="graph_increase_font_size_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Increase Font Size</property> + <property name="use_underline">True</property> + <accelerator key="Up" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="graph_decrease_font_size_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Decrease Font Size</property> + <property name="use_underline">True</property> + <accelerator key="Down" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkMenuItem" id="graph_normal_font_size_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Normal Font Size</property> + <property name="use_underline">True</property> + <accelerator key="1" signal="activate" modifiers="GDK_CONTROL_MASK"/> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="menuitem4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_parent_menuitem"> + <property name="label">_Parent</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="BackSpace" signal="activate"/> + <signal name="activate" handler="graph_parent_menuitem" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_refresh_menuitem"> + <property name="label">gtk-refresh</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="F5" signal="activate"/> + <signal name="activate" handler="graph_refresh_menuitem" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_fullscreen_menuitem"> + <property name="label">gtk-fullscreen</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <accelerator key="F11" signal="activate"/> + <signal name="activate" handler="graph_fullscreen_menuitem" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="view1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Windows</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_view1_activate" swapped="no"/> + <child type="submenu"> + <object class="GtkMenu" id="view1_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="graph_view_engine_window_menuitem"> + <property name="label">_Engine</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="E" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_view_engine_window_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_view_graph_tree_window_menuitem"> + <property name="label">_Graph Tree</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="T" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_view_tree_window_menuitem_activate" swapped="no"/> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_view_messages_window_menuitem"> + <property name="label">_Messages</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + <accelerator key="M" signal="activate" modifiers="GDK_CONTROL_MASK"/> + <signal name="activate" handler="on_graph_view_messages_window_menuitem_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkMenuItem" id="help_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Help</property> + <property name="use_underline">True</property> + <signal name="activate" handler="on_help_menu_activate" swapped="no"/> + <child type="submenu"> + <object class="GtkMenu" id="help_menu_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="right-click_the_canvas_to_add_objects1"> + <property name="label">Right-click the canvas to add objects</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="graph_help_about_menuitem"> + <property name="label">gtk-about</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_about1_activate" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHPaned" id="graph_documentation_paned"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkAlignment" id="graph_win_alignment"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="resize">True</property> + <property name="shrink">False</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="graph_documentation_scrolledwindow"> + <property name="can_focus">False</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="resize">False</property> + <property name="shrink">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkStatusbar" id="graph_win_status_bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkFileChooserDialog" id="load_graph_win"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Load Graph - Ingen</property> + <property name="window_position">center-on-parent</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="vbox11"> + <property name="can_focus">False</property> + <property name="spacing">24</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="hbuttonbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="load_graph_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="load_graph_ok_button"> + <property name="label">gtk-open</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xscale">0</property> + <child> + <object class="GtkTable" id="table14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">3</property> + <property name="column_spacing">12</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkLabel" id="load_graph_poly_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Polyphony: </property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="load_graph_poly_from_file_radio"> + <property name="label" translatable="yes">Load from _File</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="has_tooltip">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">load_graph_poly_voices_radio</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="load_graph_ports_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Ports: </property> + <property name="use_markup">True</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="load_graph_insert_ports_radio"> + <property name="label" translatable="yes">_Insert new ports</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="has_tooltip">True</property> + <property name="use_underline">True</property> + <property name="draw_indicator">True</property> + <property name="group">load_graph_merge_ports_radio</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="load_graph_merge_ports_radio"> + <property name="label" translatable="yes">_Merge with existing ports</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="has_tooltip">True</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox58"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkRadioButton" id="load_graph_poly_voices_radio"> + <property name="label" translatable="yes">_Voices:</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="has_tooltip">True</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="load_graph_poly_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">â—</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="load_graph_symbol_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Symbol: </property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">load_graph_symbol_entry</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkEntry" id="load_graph_symbol_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">â—</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">load_graph_cancel_button</action-widget> + <action-widget response="-5">load_graph_ok_button</action-widget> + </action-widgets> + </object> + <object class="GtkWindow" id="load_plugin_win"> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Load Plugin - Ingen</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <object class="GtkVBox" id="vbox9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">1</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow3"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">2</property> + <child> + <object class="GtkTreeView" id="load_plugin_plugins_treeview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="border_width">2</property> + <property name="reorderable">True</property> + <property name="rules_hint">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table16"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">3</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkLabel" id="label66"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">Node _Symbol:</property> + <property name="use_markup">True</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">load_plugin_name_entry</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox63"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkEntry" id="load_plugin_name_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="load_plugin_polyphonic_checkbutton"> + <property name="label" translatable="yes">_Polyphonic</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_underline">True</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">8</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + <property name="x_padding">6</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="load_plugin_search_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="secondary_icon_stock">gtk-clear</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkComboBox" id="load_plugin_filter_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <child> + <object class="GtkButton" id="load_plugin_close_button"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="load_plugin_add_button"> + <property name="label">gtk-add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="messages_win"> + <property name="width_request">400</property> + <property name="height_request">180</property> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Messages - Ingen</property> + <child> + <object class="GtkVBox" id="vbox12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkTextView" id="messages_textview"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="pixels_above_lines">1</property> + <property name="pixels_below_lines">1</property> + <property name="editable">False</property> + <property name="wrap_mode">word</property> + <property name="left_margin">5</property> + <property name="right_margin">5</property> + <property name="cursor_visible">False</property> + <property name="accepts_tab">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="messages_clear_button"> + <property name="label">gtk-clear</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="messages_close_button"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="new_subgraph_win"> + <property name="width_request">320</property> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Create Subgraph - Ingen</property> + <property name="resizable">False</property> + <property name="window_position">center-on-parent</property> + <property name="type_hint">dialog</property> + <child> + <object class="GtkVBox" id="vbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <child> + <object class="GtkLabel" id="label8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Symbol: </property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">new_subgraph_name_entry</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_EXPAND</property> + <property name="x_padding">5</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label9"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">_Polyphony: </property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">new_subgraph_polyphony_spinbutton</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_EXPAND</property> + <property name="x_padding">5</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="new_subgraph_polyphony_spinbutton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">â—</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + <property name="y_padding">4</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="new_subgraph_name_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">â—</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + <property name="y_padding">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="new_subgraph_message_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">4</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="new_subgraph_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="new_subgraph_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkMenu" id="object_menu"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkImageMenuItem" id="node_popup_gui_menuitem"> + <property name="label" translatable="yes">Show GUI...</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="node_embed_gui_menuitem"> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Embed GUI</property> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="node_enabled_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Enabled</property> + <property name="active">True</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="node_randomize_menuitem"> + <property name="label" translatable="yes">Randomi_ze</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="port_set_min_menuitem"> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Set Value as Mi_nimum</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="port_set_max_menuitem"> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Set Value as Ma_ximum</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="port_reset_range_menuitem"> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Re_set Range</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="port_expose_menuitem"> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Expose</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="object_menu_separator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> + <object class="GtkCheckMenuItem" id="object_polyphonic_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">P_olyphonic</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="object_learn_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Learn</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkMenuItem" id="object_unlearn_menuitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Unlearn</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="object_disconnect_menuitem"> + <property name="label">Dis_connect</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="object_destroy_menuitem"> + <property name="label">gtk-delete</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="object_rename_menuitem"> + <property name="label">_Rename...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + <child> + <object class="GtkImageMenuItem" id="object_properties_menuitem"> + <property name="label">_Properties...</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="use_underline">True</property> + <property name="use_stock">False</property> + </object> + </child> + </object> + <object class="GtkMenu" id="port_control_menu"> + <property name="can_focus">False</property> + <child> + <object class="GtkImageMenuItem" id="port_control_menu_properties"> + <property name="label">gtk-properties</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="use_stock">True</property> + <signal name="activate" handler="on_port_control_menu_properties_activate" swapped="no"/> + </object> + </child> + </object> + <object class="GtkWindow" id="port_properties_win"> + <property name="can_focus">False</property> + <property name="border_width">8</property> + <property name="title" translatable="yes">Port Properties - Ingen</property> + <property name="resizable">False</property> + <property name="window_position">mouse</property> + <child> + <object class="GtkVBox" id="dialog-vbox7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">8</property> + <child> + <object class="GtkHButtonBox" id="dialog-action_area7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="port_properties_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="port_properties_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table20"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <property name="column_spacing">2</property> + <property name="row_spacing">4</property> + <child> + <object class="GtkSpinButton" id="port_properties_min_spinner"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + <property name="digits">5</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="port_properties_max_spinner"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + <property name="digits">5</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label138"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Minimum Value: </property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + <child> + <object class="GtkLabel" id="label139"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Maximum Value: </property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options"/> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="properties_win"> + <property name="can_focus">False</property> + <property name="border_width">12</property> + <property name="window_position">center-on-parent</property> + <child> + <object class="GtkVBox" id="properties_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">12</property> + <child> + <object class="GtkScrolledWindow" id="properties_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkViewport" id="viewport2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resize_mode">queue</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkTable" id="properties_table"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_columns">3</property> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <child> + <object class="GtkComboBox" id="properties_key_combo"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="properties_value_bin"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="properties_add_button"> + <property name="label">gtk-add</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="properties_buttonbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="properties_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="properties_apply_button"> + <property name="label">gtk-apply</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="properties_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="rename_win"> + <property name="width_request">250</property> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Rename</property> + <property name="window_position">center-on-parent</property> + <property name="destroy_with_parent">True</property> + <property name="type_hint">dialog</property> + <child> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <child> + <object class="GtkTable" id="table2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <property name="row_spacing">8</property> + <child> + <object class="GtkLabel" id="label95"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Symbol: </property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">rename_symbol_entry</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="rename_label_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">â—</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">_Label: </property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">rename_label_entry</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="rename_symbol_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="invisible_char">â—</property> + <property name="activates_default">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="rename_message_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">12</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkHButtonBox" id="hbuttonbox4"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">8</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="rename_cancel_button"> + <property name="label">gtk-cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="rename_ok_button"> + <property name="label">gtk-ok</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="receives_default">False</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + </child> + </object> + <object class="GtkWindow" id="warehouse_win"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Warehouse - Ingen</property> + <child> + <object class="GtkTable" id="table8"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">6</property> + <property name="column_spacing">12</property> + <property name="row_spacing">12</property> + <child> + <object class="GtkVBox" id="toggle_control"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkHBox" id="hbox2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkLabel" id="toggle_control_name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">4</property> + <property name="label" translatable="yes"><b>Name</b></property> + <property name="use_markup">True</property> + <property name="single_line_mode">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="yalign">1</property> + <property name="yscale">0</property> + <property name="bottom_padding">1</property> + <property name="left_padding">1</property> + <property name="right_padding">4</property> + <child> + <object class="GtkHSeparator" id="hseparator7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="toggle_control_check"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">4</property> + <property name="bottom_attach">5</property> + <property name="y_padding">8</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="control_panel_vbox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkAlignment" id="alignment6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="yalign">0</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwin1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">never</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkVBox" id="control_panel_controls_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkVBox" id="graph_view_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkHBox" id="hbox70"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkToolbar" id="toolbar6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="toolbar_style">icons</property> + <property name="icon_size">1</property> + <child> + <object class="GtkToolItem" id="graph_view_breadcrumb_container"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToolbar" id="graph_view_toolbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="toolbar_style">icons</property> + <property name="show_arrow">False</property> + <property name="icon_size">1</property> + <child> + <object class="GtkToggleToolButton" id="graph_view_process_but"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="use_underline">True</property> + <property name="stock_id">gtk-execute</property> + <property name="active">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="toolitem7"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkImage" id="image1978"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xpad">4</property> + <property name="stock">gtk-copy</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="toolitem10"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkSpinButton" id="graph_view_poly_spin"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="climb_rate">1</property> + <property name="numeric">True</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow" id="graph_view_scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="can_default">True</property> + <property name="has_default">True</property> + <property name="events">GDK_EXPOSURE_MASK | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_MOTION_MASK | GDK_BUTTON1_MOTION_MASK | GDK_BUTTON2_MOTION_MASK | GDK_BUTTON3_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_FOCUS_CHANGE_MASK | GDK_STRUCTURE_MASK | GDK_PROPERTY_CHANGE_MASK | GDK_VISIBILITY_NOTIFY_MASK | GDK_PROXIMITY_IN_MASK | GDK_PROXIMITY_OUT_MASK | GDK_SUBSTRUCTURE_MASK | GDK_SCROLL_MASK</property> + <property name="border_width">1</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkHSeparator" id="hseparator5"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="control_strip"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkHBox" id="hbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkLabel" id="control_strip_name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="yalign">1</property> + <property name="xpad">4</property> + <property name="label" translatable="yes"><b>Name</b></property> + <property name="use_markup">True</property> + <property name="single_line_mode">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkAlignment" id="alignment3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="yalign">1</property> + <property name="yscale">0</property> + <property name="bottom_padding">1</property> + <property name="left_padding">1</property> + <property name="right_padding">4</property> + <child> + <object class="GtkHSeparator" id="hseparator6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="control_strip_spinner"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="width_chars">12</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + <property name="digits">4</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkHScale" id="control_strip_slider"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="digits">63</property> + <property name="draw_value">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">3</property> + <property name="bottom_attach">4</property> + <property name="y_padding">8</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="string_control"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkHBox" id="hbox3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <object class="GtkLabel" id="string_control_name_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">4</property> + <property name="label" translatable="yes"><b>Name</b></property> + <property name="use_markup">True</property> + <property name="single_line_mode">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="string_control_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">â—</property> + <property name="caps_lock_warning">False</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="top_attach">5</property> + <property name="bottom_attach">6</property> + <property name="y_padding">8</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/src/gui/ingen_gui_lv2.cpp b/src/gui/ingen_gui_lv2.cpp new file mode 100644 index 00000000..57881741 --- /dev/null +++ b/src/gui/ingen_gui_lv2.cpp @@ -0,0 +1,209 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/AtomReader.hpp" +#include "ingen/AtomSink.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/World.hpp" +#include "ingen/client/ClientStore.hpp" +#include "ingen/client/GraphModel.hpp" +#include "ingen/client/SigClientInterface.hpp" +#include "ingen/ingen.h" +#include "ingen/runtime_paths.hpp" +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/extensions/ui/ui.h" + +#include "App.hpp" +#include "GraphBox.hpp" + +#define INGEN_LV2_UI_URI INGEN_NS "GraphUIGtk2" + +namespace Ingen { + +/** A sink that writes atoms to a port via the UI extension. */ +struct IngenLV2AtomSink : public AtomSink { + IngenLV2AtomSink(URIs& uris, + LV2UI_Write_Function ui_write, + LV2UI_Controller ui_controller) + : _uris(uris) + , _ui_write(ui_write) + , _ui_controller(ui_controller) + {} + + bool write(const LV2_Atom* atom, int32_t default_id) { + _ui_write(_ui_controller, + 0, + lv2_atom_total_size(atom), + _uris.atom_eventTransfer, + atom); + return true; + } + + URIs& _uris; + LV2UI_Write_Function _ui_write; + LV2UI_Controller _ui_controller; +}; + +struct IngenLV2UI { + IngenLV2UI() + : argc(0) + , argv(nullptr) + , forge(nullptr) + , world(nullptr) + , sink(nullptr) + {} + + int argc; + char** argv; + Forge* forge; + World* world; + IngenLV2AtomSink* sink; + SPtr<GUI::App> app; + SPtr<GUI::GraphBox> view; + SPtr<Interface> engine; + SPtr<AtomReader> reader; + SPtr<Client::SigClientInterface> client; +}; + +} // namespace Ingen + +static LV2UI_Handle +instantiate(const LV2UI_Descriptor* descriptor, + const char* plugin_uri, + const char* bundle_path, + LV2UI_Write_Function write_function, + LV2UI_Controller controller, + LV2UI_Widget* widget, + const LV2_Feature* const* features) +{ +#if __cplusplus >= 201103L + using Ingen::SPtr; +#endif + + Ingen::set_bundle_path(bundle_path); + + Ingen::IngenLV2UI* ui = new Ingen::IngenLV2UI(); + + LV2_URID_Map* map = nullptr; + LV2_URID_Unmap* unmap = nullptr; + LV2_Log_Log* log = nullptr; + for (int i = 0; features[i]; ++i) { + if (!strcmp(features[i]->URI, LV2_URID__map)) { + map = (LV2_URID_Map*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_URID__unmap)) { + unmap = (LV2_URID_Unmap*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_LOG__log)) { + log = (LV2_Log_Log*)features[i]->data; + } + } + + ui->world = new Ingen::World(map, unmap, log); + ui->forge = new Ingen::Forge(ui->world->uri_map()); + + ui->world->load_configuration(ui->argc, ui->argv); + + if (!ui->world->load_module("client")) { + delete ui; + return nullptr; + } + + ui->sink = new Ingen::IngenLV2AtomSink( + ui->world->uris(), write_function, controller); + + // Set up an engine interface that writes LV2 atoms + ui->engine = SPtr<Ingen::Interface>( + new Ingen::AtomWriter( + ui->world->uri_map(), ui->world->uris(), *ui->sink)); + + ui->world->set_interface(ui->engine); + + // Create App and client + ui->app = Ingen::GUI::App::create(ui->world); + ui->client = SPtr<Ingen::Client::SigClientInterface>( + new Ingen::Client::SigClientInterface()); + ui->app->set_is_plugin(true); + ui->app->attach(ui->client); + + ui->reader = SPtr<Ingen::AtomReader>( + new Ingen::AtomReader(ui->world->uri_map(), + ui->world->uris(), + ui->world->log(), + *ui->client.get())); + + // Create empty root graph model + Ingen::Properties props; + props.emplace(ui->app->uris().rdf_type, + Ingen::Property(ui->app->uris().ingen_Graph)); + ui->app->store()->put(Ingen::main_uri(), props); + + // Create a GraphBox for the root and set as the UI widget + SPtr<const Ingen::Client::GraphModel> root = + Ingen::dynamic_ptr_cast<const Ingen::Client::GraphModel>( + ui->app->store()->object(Raul::Path("/"))); + ui->view = Ingen::GUI::GraphBox::create(*ui->app, root); + ui->view->unparent(); + *widget = ui->view->gobj(); + + // Request the actual root graph + ui->world->interface()->get(Ingen::main_uri()); + + return ui; +} + +static void +cleanup(LV2UI_Handle handle) +{ + Ingen::IngenLV2UI* ui = (Ingen::IngenLV2UI*)handle; + delete ui; +} + +static void +port_event(LV2UI_Handle handle, + uint32_t port_index, + uint32_t buffer_size, + uint32_t format, + const void* buffer) +{ + Ingen::IngenLV2UI* ui = (Ingen::IngenLV2UI*)handle; + const LV2_Atom* atom = (const LV2_Atom*)buffer; + ui->reader->write(atom); +} + +static const void* +extension_data(const char* uri) +{ + return nullptr; +} + +static const LV2UI_Descriptor descriptor = { + INGEN_LV2_UI_URI, + instantiate, + cleanup, + port_event, + extension_data +}; + +LV2_SYMBOL_EXPORT +const LV2UI_Descriptor* +lv2ui_descriptor(uint32_t index) +{ + switch (index) { + case 0: + return &descriptor; + default: + return nullptr; + } +} diff --git a/src/gui/ingen_style.rc b/src/gui/ingen_style.rc new file mode 100644 index 00000000..4763e12a --- /dev/null +++ b/src/gui/ingen_style.rc @@ -0,0 +1,155 @@ +style "ingen-default" +{ + GtkMenuItem::selected_shadow_type = out + + GtkWidget::interior_focus = 1 + GtkWidget::focus_padding = 1 + + GtkButton::default_border = { 0, 0, 0, 0 } + GtkButton::default_outside_border = { 0, 0, 0, 0 } + + GtkCheckButton::indicator_size = 12 + GtkExpander::expander_size = 16 + GtkMenuBar::internal-padding = 0 + GtkPaned::handle_size = 6 + GtkRange::slider_width = 15 + GtkRange::stepper_size = 15 + GtkRange::trough_border = 0 + GtkScrollbar::min_slider_length = 30 + GtkTreeView::expander_size = 14 + GtkTreeView::odd_row_color = "#343" + + xthickness = 1 + ythickness = 1 + + fg[NORMAL] = "#B8BBB9" + fg[PRELIGHT] = "#B8BBB9" + fg[ACTIVE] = "#B8BBB9" + fg[SELECTED] = "#B8BBB9" + fg[INSENSITIVE] = "#48494B" + + bg[NORMAL] = "#1E2224" + bg[PRELIGHT] = "#333537" + bg[ACTIVE] = "#333537" + bg[SELECTED] = "#00A150" + bg[INSENSITIVE] = "#1E2224" + + base[NORMAL] = "#111" + base[PRELIGHT] = "#222" + base[ACTIVE] = "#0A2" + base[SELECTED] = "#0A2" + base[INSENSITIVE] = "#444" + + text[NORMAL] = "#FFF" + text[PRELIGHT] = "#FFF" + text[ACTIVE] = "#FFF" + text[SELECTED] = "#FFF" + text[INSENSITIVE] = "#666" + + engine "clearlooks" + { + contrast = 1.0 + } +} + +style "ingen-progressbar" = "ingen-default" +{ + xthickness = 1 + ythickness = 1 +} + +style "ingen-wide" = "ingen-default" +{ + xthickness = 2 + ythickness = 2 +} + +style "ingen-notebook" = "ingen-wide" +{ + bg[NORMAL] = "#383B39" + bg[ACTIVE] = "#383B39" +} + +style "ingen-tasklist" = "ingen-default" +{ + xthickness = 5 + ythickness = 3 +} + +style "ingen-menu" = "ingen-default" +{ + xthickness = 5 + ythickness = 5 + bg[NORMAL] = "#262626" +} + +style "ingen-menu-item" = "ingen-default" +{ + xthickness = 2 + ythickness = 3 +} + +style "ingen-menu-itembar" = "ingen-default" +{ + xthickness = 3 + ythickness = 3 +} + +style "ingen-tree" = "ingen-default" +{ + xthickness = 2 + ythickness = 2 +} + +style "ingen-frame-title" = "ingen-default" +{ + fg[NORMAL] = "#B8BBB9" +} + +style "ingen-panel" = "ingen-default" +{ + xthickness = 3 + ythickness = 3 +} + +style "ingen-tooltips" = "ingen-default" +{ + xthickness = 4 + ythickness = 4 + bg[NORMAL] = "#585B59" +} + +style "ingen-combo" = "ingen-default" +{ + xthickness = 1 + ythickness = 2 +} + +class "*Ingen*GtkWidget" style : highest "ingen-default" +class "*Ingen*GtkButton" style : highest "ingen-wide" +class "*Ingen*GtkRange" style : highest "ingen-wide" +class "*Ingen*GtkFrame" style : highest "ingen-wide" +class "*Ingen*GtkStatusbar" style : highest "ingen-wide" +class "*Ingen*GtkMenu" style : highest "ingen-menu" +class "*Ingen*GtkMenuItem" style : highest "ingen-menu-item" +widget_class "*Ingen*MenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkAccelMenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkRadioMenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkCheckMenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkImageMenuItem.*" style : highest "ingen-menu-item" +widget_class "*Ingen*.GtkSeparatorMenuItem.*" style : highest "ingen-menu-item" +class "*Ingen*GtkEntry" style : highest "ingen-wide" +widget_class "*Ingen*.tooltips.*.GtkToggleButton" style : highest "ingen-tasklist" +widget_class "*Ingen*.GtkTreeView.GtkButton" style : highest "ingen-tree" +widget_class "*Ingen*.GtkCTree.GtkButton" style : highest "ingen-tree" +widget_class "*Ingen*.GtkList.GtkButton" style : highest "ingen-tree" +widget_class "*Ingen*.GtkCList.GtkButton" style : highest "ingen-tree" +widget_class "*Ingen*.GtkFrame.GtkLabel" style : highest "ingen-frame-title" +widget_class "*Ingen*BasePWidget.GtkEventBox.GtkTable.GtkFrame" style : highest "ingen-panel" +widget "gtk-tooltips" style : highest "ingen-tooltips" +class "*Ingen*GtkNotebook" style : highest "ingen-notebook" +class "*Ingen*GtkProgressBar" style : highest "ingen-progressbar" +widget_class "*Ingen*.GtkComboBox.GtkButton" style : highest "ingen-combo" +widget_class "*Ingen*.GtkCombo.GtkButton" style : highest "ingen-combo" + +widget "*Ingen*" style : highest "ingen-default" diff --git a/src/gui/rgba.hpp b/src/gui/rgba.hpp new file mode 100644 index 00000000..dae3f179 --- /dev/null +++ b/src/gui/rgba.hpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_GUI_RGBA_HPP +#define INGEN_GUI_RGBA_HPP + +#include <cmath> + +namespace Ingen { +namespace GUI { + +static inline uint32_t +rgba_to_uint(uint8_t r, uint8_t g, uint8_t b, uint8_t a) +{ + return ((((uint32_t)(r)) << 24) | + (((uint32_t)(g)) << 16) | + (((uint32_t)(b)) << 8) | + (((uint32_t)(a)))); +} + +static inline uint8_t +mono_interpolate(uint8_t v1, uint8_t v2, float f) +{ + return ((int)rint((v2) * (f) + (v1) * (1 - (f)))); +} + +#define RGBA_R(x) (((uint32_t)(x)) >> 24) +#define RGBA_G(x) ((((uint32_t)(x)) >> 16) & 0xFF) +#define RGBA_B(x) ((((uint32_t)(x)) >> 8) & 0xFF) +#define RGBA_A(x) (((uint32_t)(x)) & 0xFF) + +static inline uint32_t +rgba_interpolate(uint32_t c1, uint32_t c2, float f) +{ + return rgba_to_uint( + mono_interpolate(RGBA_R(c1), RGBA_R(c2), f), + mono_interpolate(RGBA_G(c1), RGBA_G(c2), f), + mono_interpolate(RGBA_B(c1), RGBA_B(c2), f), + mono_interpolate(RGBA_A(c1), RGBA_A(c2), f)); +} + +} // namespace GUI +} // namespace Ingen + +#endif // INGEN_GUI_RGBA_HPP diff --git a/src/gui/wscript b/src/gui/wscript new file mode 100644 index 00000000..160afc03 --- /dev/null +++ b/src/gui/wscript @@ -0,0 +1,113 @@ +#!/usr/bin/env python +import waflib.extras.autowaf as autowaf +import waflib.Utils as Utils +import waflib.Options as Options + +def options(ctx): + opt = ctx.get_option_group('Configuration options') + opt.add_option('--light-theme', action='store_true', dest='light_theme', + help='use light coloured theme') + +def configure(conf): + autowaf.check_pkg(conf, 'glibmm-2.4', uselib_store='GLIBMM', + atleast_version='2.14.0', mandatory=False) + autowaf.check_pkg(conf, 'gthread-2.0', uselib_store='GTHREAD', + atleast_version='2.14.0', mandatory=False) + autowaf.check_pkg(conf, 'gtkmm-2.4', uselib_store='GTKMM', + atleast_version='2.12.0', mandatory=False) + autowaf.check_pkg(conf, 'gtkmm-2.4', uselib_store='NEW_GTKMM', + atleast_version='2.14.0', mandatory=False) + autowaf.check_pkg(conf, 'ganv-1', uselib_store='GANV', + atleast_version='1.5.4', mandatory=False) + if not Options.options.no_webkit: + autowaf.check_pkg(conf, 'webkit-1.0', uselib_store='WEBKIT', + atleast_version='1.4.0', mandatory=False) + + if conf.env.HAVE_GANV and conf.env.HAVE_GTKMM: + autowaf.define(conf, 'INGEN_BUILD_GUI', 1) + + if Options.options.light_theme: + autowaf.define(conf, 'INGEN_USE_LIGHT_THEME', 1) + +def build(bld): + obj = bld(features = 'cxx cxxshlib', + export_includes = ['../..'], + includes = ['../..'], + name = 'libingen_gui', + target = 'ingen_gui', + install_path = '${LIBDIR}', + use = 'libingen libingen_client') + autowaf.use_lib(bld, obj, ''' + GANV + GLADEMM + GLIBMM + GNOMECANVAS + GTKMM + LILV + LV2 + RAUL + SIGCPP + SORD + SOUP + SUIL + WEBKIT + ''') + + obj.source = ''' + App.cpp + Arc.cpp + BreadCrumbs.cpp + ConnectWindow.cpp + GraphBox.cpp + GraphCanvas.cpp + GraphPortModule.cpp + GraphTreeWindow.cpp + GraphView.cpp + GraphWindow.cpp + LoadGraphWindow.cpp + LoadPluginWindow.cpp + MessagesWindow.cpp + NewSubgraphWindow.cpp + NodeMenu.cpp + NodeModule.cpp + ObjectMenu.cpp + PluginMenu.cpp + Port.cpp + PortMenu.cpp + PropertiesWindow.cpp + RDFS.cpp + RenameWindow.cpp + Style.cpp + SubgraphModule.cpp + ThreadedLoader.cpp + URIEntry.cpp + WidgetFactory.cpp + WindowFactory.cpp + ingen_gui.cpp + ''' + + # XML UI definition + bld(features = 'subst', + source = 'ingen_gui.ui', + target = '../../ingen_gui.ui', + install_path = '${DATADIR}/ingen', + chmod = Utils.O755, + INGEN_VERSION = bld.env.INGEN_VERSION) + + # Gtk style + bld(features = 'subst', + is_copy = True, + source = 'ingen_style.rc', + target = '../../ingen_style.rc', + install_path = '${DATADIR}/ingen', + chmod = Utils.O755) + + # LV2 UI + obj = bld(features = 'cxx cxxshlib', + source = 'ingen_gui_lv2.cpp', + includes = ['.', '../..'], + name = 'ingen_gui_lv2', + target = 'ingen_gui_lv2', + install_path = '${LV2DIR}/ingen.lv2/', + use = 'libingen libingen_gui') + autowaf.use_lib(bld, obj, 'LV2 SERD SORD LILV RAUL GLIBMM GTKMM') diff --git a/src/ingen/ingen.cpp b/src/ingen/ingen.cpp new file mode 100644 index 00000000..d812d862 --- /dev/null +++ b/src/ingen/ingen.cpp @@ -0,0 +1,263 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <signal.h> + +#include <cstdlib> +#include <chrono> +#include <iostream> +#include <memory> +#include <string> + +#include "raul/Path.hpp" + +#include "ingen_config.h" + +#include "ingen/Configuration.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Parser.hpp" +#include "ingen/World.hpp" +#include "ingen/paths.hpp" +#include "ingen/runtime_paths.hpp" +#include "ingen/types.hpp" +#ifdef HAVE_SOCKET +#include "ingen/client/SocketClient.hpp" +#endif + +using namespace std; +using namespace Ingen; + +class DummyInterface : public Interface +{ + URI uri() const override { return URI("ingen:dummy"); } + void message(const Message& msg) override {} +}; + +unique_ptr<Ingen::World> world; + +static void +ingen_interrupt(int signal) +{ + if (signal == SIGTERM) { + cerr << "ingen: Terminated" << endl; + exit(EXIT_FAILURE); + } else { + cout << "ingen: Interrupted" << endl; + if (world && world->engine()) { + world->engine()->quit(); + } + } +} + +static void +ingen_try(bool cond, const char* msg) +{ + if (!cond) { + cerr << "ingen: error: " << msg << endl; + exit(EXIT_FAILURE); + } +} + +static int +print_version() +{ + cout << "ingen " << INGEN_VERSION + << " <http://drobilla.net/software/ingen>\n" + << "Copyright 2007-2017 David Robillard <http://drobilla.net>.\n" + << "License: <https://www.gnu.org/licenses/agpl-3.0>\n" + << "This is free software; you are free to change and redistribute it.\n" + << "There is NO WARRANTY, to the extent permitted by law." << endl; + return EXIT_SUCCESS; +} + +int +main(int argc, char** argv) +{ + Ingen::set_bundle_path_from_code((void*)&print_version); + + // Create world + try { + world = unique_ptr<Ingen::World>(new Ingen::World(nullptr, NULL, NULL)); + world->load_configuration(argc, argv); + if (argc <= 1) { + world->conf().print_usage("ingen", cout); + return EXIT_FAILURE; + } else if (world->conf().option("help").get<int32_t>()) { + world->conf().print_usage("ingen", cout); + return EXIT_SUCCESS; + } else if (world->conf().option("version").get<int32_t>()) { + return print_version(); + } + } catch (std::exception& e) { + cout << "ingen: error: " << e.what() << endl; + return EXIT_FAILURE; + } + + Configuration& conf = world->conf(); + if (conf.option("uuid").is_valid()) { + world->set_jack_uuid(conf.option("uuid").ptr<char>()); + } + + // Run engine + if (conf.option("engine").get<int32_t>()) { + if (world->conf().option("threads").get<int32_t>() < 1) { + cerr << "ingen: error: threads must be > 0" << endl; + return EXIT_FAILURE; + } + + ingen_try(world->load_module("server"), "Failed to load server module"); + + ingen_try(bool(world->engine()), "Unable to create engine"); + world->engine()->listen(); + } + +#ifdef HAVE_SOCKET + Client::SocketClient::register_factories(world.get()); +#endif + + // Load GUI if requested + if (conf.option("gui").get<int32_t>()) { + ingen_try(world->load_module("client"), "Failed to load client module"); + ingen_try(world->load_module("gui"), "Failed to load GUI module"); + } + + // If we don't have a local engine interface (from the GUI), use network + SPtr<Interface> engine_interface(world->interface()); + SPtr<Interface> dummy_client(new DummyInterface()); + if (!engine_interface) { + const char* const uri = conf.option("connect").ptr<char>(); + ingen_try(URI::is_valid(uri), + (fmt("Invalid URI <%1%>") % uri).str().c_str()); + engine_interface = world->new_interface(URI(uri), dummy_client); + + if (!engine_interface && !conf.option("gui").get<int32_t>()) { + cerr << (fmt("ingen: error: Failed to connect to `%1%'\n") % uri); + return EXIT_FAILURE; + } + + world->set_interface(engine_interface); + } + + // Activate the engine, if we have one + if (world->engine()) { + if (!world->load_module("jack") && !world->load_module("portaudio")) { + cerr << "ingen: error: Failed to load driver module" << endl; + return EXIT_FAILURE; + } + + if (!world->engine()->supports_dynamic_ports() && + !conf.option("load").is_valid()) { + cerr << "ingen: error: Initial graph required for driver" << endl; + return EXIT_FAILURE; + } + } + + // Load a graph + if (conf.option("load").is_valid()) { + boost::optional<Raul::Path> parent; + boost::optional<Raul::Symbol> symbol; + + const Atom& path_option = conf.option("path"); + if (path_option.is_valid()) { + if (Raul::Path::is_valid(path_option.ptr<char>())) { + const Raul::Path p(path_option.ptr<char>()); + if (!p.is_root()) { + parent = p.parent(); + symbol = Raul::Symbol(p.symbol()); + } + } else { + cerr << "Invalid path given: '" << path_option.ptr<char>() << endl; + } + } + + ingen_try(bool(world->parser()), "Failed to create parser"); + + const string graph = conf.option("load").ptr<char>(); + + engine_interface->get(URI("ingen:/plugins")); + engine_interface->get(main_uri()); + + std::lock_guard<std::mutex> lock(world->rdf_mutex()); + world->parser()->parse_file( + world.get(), engine_interface.get(), graph, parent, symbol); + } else if (conf.option("server-load").is_valid()) { + const char* path = conf.option("server-load").ptr<char>(); + if (serd_uri_string_has_scheme((const uint8_t*)path)) { + std::cout << "Loading " << path << " (server side)" << std::endl; + engine_interface->copy(URI(path), main_uri()); + } else { + SerdNode uri = serd_node_new_file_uri( + (const uint8_t*)path, nullptr, nullptr, true); + std::cout << "Loading " << (const char*)uri.buf + << " (server side)" << std::endl; + engine_interface->copy(URI((const char*)uri.buf), main_uri()); + serd_node_free(&uri); + } + } + + // Save the currently loaded graph + if (conf.option("save").is_valid()) { + const char* path = conf.option("save").ptr<char>(); + if (serd_uri_string_has_scheme((const uint8_t*)path)) { + std::cout << "Saving to " << path << std::endl; + engine_interface->copy(main_uri(), URI(path)); + } else { + SerdNode uri = serd_node_new_file_uri( + (const uint8_t*)path, nullptr, nullptr, true); + std::cout << "Saving to " << (const char*)uri.buf << std::endl; + engine_interface->copy(main_uri(), URI((const char*)uri.buf)); + serd_node_free(&uri); + } + } + + // Activate the engine now that the graph is loaded + if (world->engine()) { + world->engine()->flush_events(std::chrono::milliseconds(10)); + world->engine()->activate(); + } + + // Set up signal handlers that will set quit_flag on interrupt + signal(SIGINT, ingen_interrupt); + signal(SIGTERM, ingen_interrupt); + + if (conf.option("gui").get<int32_t>()) { + world->run_module("gui"); + } else if (world->engine()) { + // Run engine main loop until interrupt + while (world->engine()->main_iteration()) { + this_thread::sleep_for(chrono::milliseconds(125)); + } + } + + // Sleep for a half second to allow event queues to drain + this_thread::sleep_for(chrono::milliseconds(500)); + + // Shut down + if (world->engine()) { + world->engine()->deactivate(); + } + + // Save configuration to restore preferences on next run + const std::string path = conf.save( + world->uri_map(), "ingen", "options.ttl", Configuration::GLOBAL); + std::cout << (fmt("Saved configuration to %1%") % path) << std::endl; + + engine_interface.reset(); + + return 0; +} diff --git a/src/ingen/ingen.desktop b/src/ingen/ingen.desktop new file mode 100644 index 00000000..99192435 --- /dev/null +++ b/src/ingen/ingen.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=Ingen +Comment=Create synthesizers and effects in a modular environment +Exec=ingen -eg +Terminal=false +Icon=ingen +Type=Application +Categories=Application;AudioVideo;Sound;Audio; diff --git a/src/ingen/ingen.grind b/src/ingen/ingen.grind new file mode 100644 index 00000000..9e60915b --- /dev/null +++ b/src/ingen/ingen.grind @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +export INGEN_MODULE_PATH="`pwd`/../../libs/engine/.libs:`pwd`/../../libs/gui/.libs:`pwd`/../../libs/client/.libs" +export INGEN_GLADE_PATH="`pwd`/../../libs/gui/ingen_gui.glade" +libtool --mode=execute valgrind ./ingen $@ diff --git a/src/runtime_paths.cpp b/src/runtime_paths.cpp new file mode 100644 index 00000000..8dbe5c0c --- /dev/null +++ b/src/runtime_paths.cpp @@ -0,0 +1,146 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <climits> +#include <cstdlib> +#include <cstdlib> +#include <sstream> +#include <string> + +#include <dlfcn.h> + +#include "ingen/runtime_paths.hpp" +#include "ingen/FilePath.hpp" + +#include "ingen_config.h" + +namespace Ingen { + +static FilePath bundle_path; + +#if defined(__APPLE__) +const char search_path_separator = ':'; +static const char* const library_prefix = "lib"; +static const char* const library_suffix = ".dylib"; +#elif defined(_WIN32) && !defined(__CYGWIN__) +const char search_path_separator = ';'; +static const char* const library_prefix = ""; +static const char* const library_suffix = ".dll"; +#else +const char search_path_separator = ':'; +static const char* const library_prefix = "lib"; +static const char* const library_suffix = ".so"; +#endif + +/** Must be called once at startup, and passed a pointer to a function + * that lives in the 'top level' of the bundle (e.g. the executable). + * Passing a function defined in a module etc. will not work! + */ +void +set_bundle_path_from_code(void* function) +{ + Dl_info dli; + dladdr(function, &dli); + +#ifdef BUNDLE + char bin_loc[PATH_MAX]; + realpath(dli.dli_fname, bin_loc); +#else + const char* bin_loc = dli.dli_fname; +#endif + + bundle_path = FilePath(bin_loc).parent_path(); +} + +void +set_bundle_path(const char* path) +{ + bundle_path = FilePath(path); +} + +/** Return the absolute path of a file in an Ingen LV2 bundle + */ +FilePath +bundle_file_path(const std::string& name) +{ + return bundle_path / name; +} + +/** Return the absolute path of a 'resource' file. + */ +FilePath +data_file_path(const std::string& name) +{ +#ifdef BUNDLE + return bundle_path / INGEN_DATA_DIR / name; +#else + return FilePath(INGEN_DATA_DIR) / name; +#endif +} + +/** Return the absolute path of a module (dynamically loaded shared library). + */ +FilePath +ingen_module_path(const std::string& name, FilePath dir) +{ + FilePath ret; + if (dir.empty()) { +#ifdef BUNDLE + dir = FilePath(bundle_path) / INGEN_MODULE_DIR; +#else + dir = FilePath(INGEN_MODULE_DIR); +#endif + } + + return dir / + (std::string(library_prefix) + "ingen_" + name + library_suffix); +} + +FilePath +user_config_dir() +{ + const char* const xdg_config_home = getenv("XDG_CONFIG_HOME"); + const char* const home = getenv("HOME"); + + if (xdg_config_home) { + return FilePath(xdg_config_home); + } else if (home) { + return FilePath(home) / ".config"; + } + + return FilePath(); +} + +std::vector<FilePath> +system_config_dirs() +{ + const char* const xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); + + std::vector<FilePath> paths; + if (xdg_config_dirs) { + std::istringstream ss(xdg_config_dirs); + std::string entry; + while (std::getline(ss, entry, search_path_separator)) { + paths.emplace_back(entry); + } + } else { + paths.emplace_back("/etc/xdg"); + } + + return paths; +} + +} // namespace Ingen diff --git a/src/server/ArcImpl.cpp b/src/server/ArcImpl.cpp new file mode 100644 index 00000000..5b96ca03 --- /dev/null +++ b/src/server/ArcImpl.cpp @@ -0,0 +1,114 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "ArcImpl.hpp" +#include "BlockImpl.hpp" +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "InputPort.hpp" +#include "OutputPort.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +/** Constructor for an arc from a block's output port. + * + * This handles both polyphonic and monophonic blocks, transparently to the + * user (InputPort). + */ +ArcImpl::ArcImpl(PortImpl* tail, PortImpl* head) + : _tail(tail) + , _head(head) +{ + assert(tail != head); + assert(tail->path() != head->path()); +} + +ArcImpl::~ArcImpl() +{ + if (is_linked()) { + InputPort* iport = dynamic_cast<InputPort*>(_head); + if (iport) { + iport->remove_arc(*this); + } + } +} + +const Raul::Path& +ArcImpl::tail_path() const +{ + return _tail->path(); +} + +const Raul::Path& +ArcImpl::head_path() const +{ + return _head->path(); +} + +BufferRef +ArcImpl::buffer(uint32_t voice, SampleCount offset) const +{ + return _tail->buffer(std::min(voice, _tail->poly() - 1)); +} + +bool +ArcImpl::must_mix() const +{ + return (_tail->poly() > _head->poly() || + (_tail->buffer(0)->is_sequence() != _head->buffer(0)->is_sequence())); +} + +bool +ArcImpl::can_connect(const PortImpl* src, const InputPort* dst) +{ + const Ingen::URIs& uris = src->bufs().uris(); + return ( + // (Audio | Control | CV) => (Audio | Control | CV) + ( (src->is_a(PortType::ID::CONTROL) || + src->is_a(PortType::ID::AUDIO) || + src->is_a(PortType::ID::CV)) + && (dst->is_a(PortType::ID::CONTROL) + || dst->is_a(PortType::ID::AUDIO) + || dst->is_a(PortType::ID::CV))) + + // Equal types + || (src->type() == dst->type() && + src->buffer_type() == dst->buffer_type()) + + // Control => atom:Float Value + || (src->is_a(PortType::ID::CONTROL) && dst->supports(uris.atom_Float)) + + // Audio => atom:Sound Value + || (src->is_a(PortType::ID::AUDIO) && dst->supports(uris.atom_Sound)) + + // atom:Float Value => Control + || (src->supports(uris.atom_Float) && dst->is_a(PortType::ID::CONTROL)) + + // atom:Float Value => CV + || (src->supports(uris.atom_Float) && dst->is_a(PortType::ID::CV)) + + // atom:Sound Value => Audio + || (src->supports(uris.atom_Sound) && dst->is_a(PortType::ID::AUDIO))); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/ArcImpl.hpp b/src/server/ArcImpl.hpp new file mode 100644 index 00000000..40a6d179 --- /dev/null +++ b/src/server/ArcImpl.hpp @@ -0,0 +1,84 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_ARC_IMPL_HPP +#define INGEN_ENGINE_ARC_IMPL_HPP + +#include <cstdlib> + +#include <boost/intrusive/slist.hpp> + +#include "ingen/Arc.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "raul/Deletable.hpp" + +#include "BufferRef.hpp" +#include "RunContext.hpp" + +namespace Ingen { +namespace Server { + +class PortImpl; +class InputPort; + +/** Represents a single inbound connection for an InputPort. + * + * This can be a group of ports (coming from a polyphonic Block) or + * a single Port. This class exists basically as an abstraction of mixing + * down polyphonic inputs, so InputPort can just deal with mixing down + * multiple connections (oblivious to the polyphonic situation of the + * connection itself). + * + * This is stored in an intrusive slist in InputPort. + * + * \ingroup engine + */ +class ArcImpl + : private Raul::Noncopyable + , public Arc + , public boost::intrusive::slist_base_hook<> +{ +public: + ArcImpl(PortImpl* tail, PortImpl* head); + ~ArcImpl(); + + inline PortImpl* tail() const { return _tail; } + inline PortImpl* head() const { return _head; } + + const Raul::Path& tail_path() const; + const Raul::Path& head_path() const; + + /** Get the buffer for a particular voice. + * An Arc is smart - it knows the destination port requesting the + * buffer, and will return accordingly (e.g. the same buffer for every + * voice in a mono->poly arc). + */ + BufferRef buffer(uint32_t voice, SampleCount offset=0) const; + + /** Whether this arc must mix down voices into a local buffer */ + bool must_mix() const; + + static bool can_connect(const PortImpl* src, const InputPort* dst); + +protected: + PortImpl* const _tail; + PortImpl* const _head; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_ARC_IMPL_HPP diff --git a/src/server/BlockFactory.cpp b/src/server/BlockFactory.cpp new file mode 100644 index 00000000..7dcfd6af --- /dev/null +++ b/src/server/BlockFactory.cpp @@ -0,0 +1,229 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> + +#include "lilv/lilv.h" + +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "ingen/World.hpp" +#include "internals/BlockDelay.hpp" +#include "internals/Controller.hpp" +#include "internals/Note.hpp" +#include "internals/Time.hpp" +#include "internals/Trigger.hpp" + +#include "BlockFactory.hpp" +#include "InternalPlugin.hpp" +#include "LV2Plugin.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +using namespace Internals; + +BlockFactory::BlockFactory(Ingen::World* world) + : _world(world) + , _has_loaded(false) +{ + load_internal_plugins(); +} + +BlockFactory::~BlockFactory() +{ + for (auto& p : _plugins) { + delete p.second; + } + + _plugins.clear(); +} + +const BlockFactory::Plugins& +BlockFactory::plugins() +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + if (!_has_loaded) { + load_lv2_plugins(); + _has_loaded = true; + } + return _plugins; +} + +std::set<PluginImpl*> +BlockFactory::refresh() +{ + // Record current plugins, and those that are currently zombies + const Plugins old_plugins(_plugins); + std::set<PluginImpl*> zombies; + for (const auto& p : _plugins) { + if (p.second->is_zombie()) { + zombies.insert(p.second); + } + } + + // Re-load plugins + load_lv2_plugins(); + + // Add any new plugins to response + std::set<PluginImpl*> new_plugins; + for (const auto& p : _plugins) { + auto o = old_plugins.find(p.first); + if (o == old_plugins.end()) { + new_plugins.insert(p.second); + } + } + + // Add any resurrected plugins to response + for (const auto& z : zombies) { + if (!z->is_zombie()) { + new_plugins.insert(z); + } + } + + return new_plugins; +} + +PluginImpl* +BlockFactory::plugin(const URI& uri) +{ + load_plugin(uri); + const Plugins::const_iterator i = _plugins.find(uri); + return ((i != _plugins.end()) ? i->second : nullptr); +} + +void +BlockFactory::load_internal_plugins() +{ + Ingen::URIs& uris = _world->uris(); + InternalPlugin* block_delay_plug = BlockDelayNode::internal_plugin(uris); + _plugins.emplace(block_delay_plug->uri(), block_delay_plug); + + InternalPlugin* controller_plug = ControllerNode::internal_plugin(uris); + _plugins.emplace(controller_plug->uri(), controller_plug); + + InternalPlugin* note_plug = NoteNode::internal_plugin(uris); + _plugins.emplace(note_plug->uri(), note_plug); + + InternalPlugin* time_plug = TimeNode::internal_plugin(uris); + _plugins.emplace(time_plug->uri(), time_plug); + + InternalPlugin* trigger_plug = TriggerNode::internal_plugin(uris); + _plugins.emplace(trigger_plug->uri(), trigger_plug); +} + +void +BlockFactory::load_plugin(const URI& uri) +{ + if (_has_loaded || _plugins.find(uri) != _plugins.end()) { + return; + } + + LilvNode* node = lilv_new_uri(_world->lilv_world(), uri.c_str()); + const LilvPlugins* plugs = lilv_world_get_all_plugins(_world->lilv_world()); + const LilvPlugin* plug = lilv_plugins_get_by_uri(plugs, node); + if (plug) { + LV2Plugin* const ingen_plugin = new LV2Plugin(_world, plug); + _plugins.emplace(uri, ingen_plugin); + } + lilv_node_free(node); +} + +/** Loads information about all LV2 plugins into internal plugin database. + */ +void +BlockFactory::load_lv2_plugins() +{ + // Build an array of port type nodes for checking compatibility + typedef std::vector< SPtr<LilvNode> > Types; + Types types; + for (unsigned t = PortType::ID::AUDIO; t <= PortType::ID::ATOM; ++t) { + const URI& uri(PortType((PortType::ID)t).uri()); + types.push_back( + SPtr<LilvNode>(lilv_new_uri(_world->lilv_world(), uri.c_str()), + lilv_node_free)); + } + + const LilvPlugins* plugins = lilv_world_get_all_plugins(_world->lilv_world()); + LILV_FOREACH(plugins, i, plugins) { + const LilvPlugin* lv2_plug = lilv_plugins_get(plugins, i); + const URI uri(lilv_node_as_uri(lilv_plugin_get_uri(lv2_plug))); + + // Ignore plugins that require features Ingen doesn't support + LilvNodes* features = lilv_plugin_get_required_features(lv2_plug); + bool supported = true; + LILV_FOREACH(nodes, f, features) { + const char* feature = lilv_node_as_uri(lilv_nodes_get(features, f)); + if (!_world->lv2_features().is_supported(feature)) { + supported = false; + _world->log().warn( + fmt("Ignoring <%1%>; required feature <%2%>\n") + % uri % feature); + break; + } + } + lilv_nodes_free(features); + if (!supported) { + continue; + } + + // Ignore plugins that are missing ports + if (!lilv_plugin_get_port_by_index(lv2_plug, 0)) { + _world->log().warn( + fmt("Ignoring <%1%>; missing or corrupt ports\n") % uri); + continue; + } + + const uint32_t n_ports = lilv_plugin_get_num_ports(lv2_plug); + for (uint32_t p = 0; p < n_ports; ++p) { + const LilvPort* port = lilv_plugin_get_port_by_index(lv2_plug, p); + supported = false; + for (const auto& t : types) { + if (lilv_port_is_a(lv2_plug, port, t.get())) { + supported = true; + break; + } + } + if (!supported && + !lilv_port_has_property(lv2_plug, + port, + _world->uris().lv2_connectionOptional)) { + _world->log().warn( + fmt("Ignoring <%1%>; unsupported port <%2%>\n") + % uri % lilv_node_as_string( + lilv_port_get_symbol(lv2_plug, port))); + break; + } + } + if (!supported) { + continue; + } + + auto p = _plugins.find(uri); + if (p == _plugins.end()) { + LV2Plugin* const plugin = new LV2Plugin(_world, lv2_plug); + _plugins.emplace(uri, plugin); + } else if (lilv_plugin_verify(lv2_plug)) { + p->second->set_is_zombie(false); + } + } + + _world->log().info(fmt("Loaded %1% plugins\n") % _plugins.size()); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/BlockFactory.hpp b/src/server/BlockFactory.hpp new file mode 100644 index 00000000..25885f75 --- /dev/null +++ b/src/server/BlockFactory.hpp @@ -0,0 +1,67 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BLOCKFACTORY_HPP +#define INGEN_ENGINE_BLOCKFACTORY_HPP + +#include <map> +#include <set> + +#include "ingen/World.hpp" +#include "ingen/types.hpp" +#include "raul/Noncopyable.hpp" + +namespace Ingen { +namespace Server { + +class PluginImpl; + +/** Discovers and loads plugin libraries. + * + * \ingroup engine + */ +class BlockFactory : public Raul::Noncopyable +{ +public: + explicit BlockFactory(Ingen::World* world); + ~BlockFactory(); + + /** Reload plugin list. + * + * @return The set of newly loaded plugins. + */ + std::set<PluginImpl*> refresh(); + + void load_plugin(const URI& uri); + + typedef std::map<URI, PluginImpl*> Plugins; + const Plugins& plugins(); + + PluginImpl* plugin(const URI& uri); + +private: + void load_lv2_plugins(); + void load_internal_plugins(); + + Plugins _plugins; + Ingen::World* _world; + bool _has_loaded; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BLOCKFACTORY_HPP diff --git a/src/server/BlockImpl.cpp b/src/server/BlockImpl.cpp new file mode 100644 index 00000000..e95645f9 --- /dev/null +++ b/src/server/BlockImpl.cpp @@ -0,0 +1,303 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cstdint> + +#include "raul/Array.hpp" + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "BlockImpl.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +BlockImpl::BlockImpl(PluginImpl* plugin, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : NodeImpl(plugin->uris(), parent, symbol) + , _plugin(plugin) + , _polyphony((polyphonic && parent) ? parent->internal_poly() : 1) + , _mark(Mark::UNVISITED) + , _polyphonic(polyphonic) + , _activated(false) + , _enabled(true) +{ + assert(_plugin); + assert(_polyphony > 0); +} + +BlockImpl::~BlockImpl() +{ + if (_activated) { + deactivate(); + } + + if (is_linked()) { + parent_graph()->remove_block(*this); + } +} + +Node* +BlockImpl::port(uint32_t index) const +{ + return (*_ports)[index]; +} + +const Resource* +BlockImpl::plugin() const +{ + return _plugin; +} + +const PluginImpl* +BlockImpl::plugin_impl() const +{ + return _plugin; +} + +void +BlockImpl::activate(BufferFactory& bufs) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + _activated = true; + for (uint32_t p = 0; p < num_ports(); ++p) { + PortImpl* const port = _ports->at(p); + port->activate(bufs); + } +} + +void +BlockImpl::deactivate() +{ + _activated = false; + for (uint32_t p = 0; p < num_ports(); ++p) { + PortImpl* const port = _ports->at(p); + port->deactivate(); + } +} + +bool +BlockImpl::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + if (!_polyphonic) { + poly = 1; + } + + if (_ports) { + for (uint32_t i = 0; i < _ports->size(); ++i) { + _ports->at(i)->prepare_poly(bufs, poly); + } + } + + return true; +} + +bool +BlockImpl::apply_poly(RunContext& context, uint32_t poly) +{ + if (!_polyphonic) { + poly = 1; + } + + _polyphony = poly; + + if (_ports) { + for (uint32_t i = 0; i < num_ports(); ++i) { + _ports->at(i)->apply_poly(context, poly); + } + } + + return true; +} + +void +BlockImpl::set_buffer_size(RunContext& context, + BufferFactory& bufs, + LV2_URID type, + uint32_t size) +{ + if (_ports) { + for (uint32_t i = 0; i < _ports->size(); ++i) { + PortImpl* const p = _ports->at(i); + if (p->buffer_type() == type) { + p->set_buffer_size(context, bufs, size); + } + } + } +} + +PortImpl* +BlockImpl::nth_port_by_type(uint32_t n, bool input, PortType type) +{ + uint32_t count = 0; + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + PortImpl* const port = _ports->at(i); + if (port->is_input() == input && port->type() == type) { + if (count++ == n) { + return port; + } + } + } + return nullptr; +} + +PortImpl* +BlockImpl::port_by_symbol(const char* symbol) +{ + for (uint32_t p = 0; _ports && p < _ports->size(); ++p) { + if (_ports->at(p)->symbol() == symbol) { + return _ports->at(p); + } + } + return nullptr; +} + +void +BlockImpl::pre_process(RunContext& context) +{ + // Mix down input ports + for (uint32_t i = 0; i < num_ports(); ++i) { + PortImpl* const port = _ports->at(i); + port->pre_process(context); + port->connect_buffers(); + } +} + +void +BlockImpl::bypass(RunContext& context) +{ + if (!_ports) { + return; + } + + // Prepare port buffers for reading, converting/mixing if necessary + for (uint32_t i = 0; i < _ports->size(); ++i) { + _ports->at(i)->connect_buffers(); + _ports->at(i)->pre_run(context); + } + + // Dumb bypass + for (PortType t : { PortType::AUDIO, PortType::CV, PortType::ATOM }) { + for (uint32_t i = 0;; ++i) { + PortImpl* in = nth_port_by_type(i, true, t); + PortImpl* out = nth_port_by_type(i, false, t); + if (!out) { + break; // Finished writing all outputs + } else if (in) { + // Copy corresponding input to output + for (uint32_t v = 0; v < _polyphony; ++v) { + out->buffer(v)->copy(context, in->buffer(v).get()); + } + } else { + // Output but no corresponding input, clear + for (uint32_t v = 0; v < _polyphony; ++v) { + out->buffer(v)->clear(); + } + } + } + } + post_process(context); +} + +void +BlockImpl::process(RunContext& context) +{ + pre_process(context); + + if (!_enabled) { + bypass(context); + post_process(context); + return; + } + + RunContext subcontext(context); + for (SampleCount offset = 0; offset < context.nframes();) { + // Find earliest offset of a value change + SampleCount chunk_end = context.nframes(); + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + PortImpl* const port = _ports->at(i); + if (port->type() == PortType::CONTROL && port->is_input()) { + const SampleCount o = port->next_value_offset( + offset, context.nframes()); + if (o < chunk_end) { + chunk_end = o; + } + } + } + + // Slice context into a chunk from now until the next change + subcontext.slice(offset, chunk_end - offset); + + // Prepare port buffers for reading, converting/mixing if necessary + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + _ports->at(i)->connect_buffers(offset); + _ports->at(i)->pre_run(subcontext); + } + + // Run the chunk + run(subcontext); + + // Emit control port outputs as events + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + PortImpl* const port = _ports->at(i); + if (port->type() == PortType::CONTROL && port->is_output()) { + // TODO: Only emit events when value has actually changed? + for (uint32_t v = 0; v < _polyphony; ++v) { + port->buffer(v)->append_event(offset, port->buffer(v)->value()); + } + } + } + + offset = chunk_end; + subcontext.slice(offset, chunk_end - offset); + } + + post_process(context); +} + +void +BlockImpl::post_process(RunContext& context) +{ + // Write output ports + for (uint32_t i = 0; _ports && i < _ports->size(); ++i) { + _ports->at(i)->post_process(context); + } +} + +void +BlockImpl::set_port_buffer(uint32_t voice, + uint32_t port_num, + BufferRef buf, + SampleCount offset) +{ + /*std::cout << path() << " set port " << port_num << " voice " << voice + << " buffer " << buf << " offset " << offset << std::endl;*/ +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/BlockImpl.hpp b/src/server/BlockImpl.hpp new file mode 100644 index 00000000..d663e319 --- /dev/null +++ b/src/server/BlockImpl.hpp @@ -0,0 +1,207 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BLOCKIMPL_HPP +#define INGEN_ENGINE_BLOCKIMPL_HPP + +#include <set> + +#include <boost/intrusive/slist.hpp> +#include <boost/optional.hpp> + +#include "lilv/lilv.h" + +#include "raul/Array.hpp" + +#include "BufferRef.hpp" +#include "NodeImpl.hpp" +#include "PluginImpl.hpp" +#include "PortType.hpp" +#include "RunContext.hpp" +#include "types.hpp" + +namespace Raul { +class Maid; +} + +namespace Ingen { +namespace Server { + +class Buffer; +class BufferFactory; +class Engine; +class GraphImpl; +class PluginImpl; +class PortImpl; +class RunContext; +class Worker; + +/** A Block in a Graph (which is also a Block). + * + * This is what is often called a "Module" in modular synthesizers. A Block is + * a unit with input/output ports, a process() method, and some other things. + * + * \ingroup engine + */ +class BlockImpl : public NodeImpl + , public boost::intrusive::slist_base_hook<> // In GraphImpl +{ +public: + typedef Raul::Array<PortImpl*> Ports; + + BlockImpl(PluginImpl* plugin, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate rate); + + virtual ~BlockImpl(); + + virtual GraphType graph_type() const { return GraphType::BLOCK; } + + /** Activate this Block. + * + * This function must be called in a non-realtime thread before it is + * inserted in to a graph. Any non-realtime actions that need to be + * done before the Block is ready for use should be done here. + */ + virtual void activate(BufferFactory& bufs); + + /** Deactivate this Block. + * + * This function must be called in a non-realtime thread after the + * block has been removed from its graph (i.e. processing is finished). + */ + virtual void deactivate(); + + /** Duplicate this Node. */ + virtual BlockImpl* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) { return nullptr; } + + /** Return true iff this block is activated */ + bool activated() const { return _activated; } + + /** Return true iff this block is enabled (not bypassed). */ + bool enabled() const { return _enabled; } + + /** Enable or disable (bypass) this block. */ + void set_enabled(bool e) { _enabled = e; } + + /** Load a preset from the world for this block. */ + virtual LilvState* load_preset(const URI& uri) { return nullptr; } + + /** Restore `state`. */ + virtual void apply_state(const UPtr<Worker>& worker, const LilvState* state) {} + + /** Save current state as preset. */ + virtual boost::optional<Resource> + save_preset(const URI& bundle, + const Properties& props) { return boost::optional<Resource>(); } + + /** Learn the next incoming MIDI event (for internals) */ + virtual void learn() {} + + /** Do whatever needs doing in the process thread before process() is called */ + virtual void pre_process(RunContext& context); + + /** Run block for an entire process cycle (calls run()). */ + virtual void process(RunContext& context); + + /** Bypass block for an entire process cycle (called from process()). */ + virtual void bypass(RunContext& context); + + /** Run block for a portion of process cycle (called from process()). */ + virtual void run(RunContext& context) = 0; + + /** Do whatever needs doing in the process thread after process() is called */ + virtual void post_process(RunContext& context); + + /** Set the buffer of a port to a given buffer (e.g. connect plugin to buffer) */ + virtual void set_port_buffer(uint32_t voice, + uint32_t port_num, + BufferRef buf, + SampleCount offset); + + virtual Node* port(uint32_t index) const; + virtual PortImpl* port_impl(uint32_t index) const { return (*_ports)[index]; } + + /** Get a port by symbol. */ + virtual PortImpl* port_by_symbol(const char* symbol); + + /** Blocks that are connected to this Block's inputs. */ + std::set<BlockImpl*>& providers() { return _providers; } + + /** Blocks that are connected to this Block's outputs. */ + std::set<BlockImpl*>& dependants() { return _dependants; } + + /** Flag block as polyphonic. + * + * Note this will not actually allocate voices etc., prepare_poly + * and apply_poly must be called after this function to truly make + * a block polyphonic. + */ + virtual void set_polyphonic(bool p) { _polyphonic = p; } + + virtual bool prepare_poly(BufferFactory& bufs, uint32_t poly); + virtual bool apply_poly(RunContext& context, uint32_t poly); + + /** Information about the Plugin this Block is an instance of. + * Not the best name - not all blocks come from plugins (ie Graph) + */ + virtual const Resource* plugin() const; + + /** Information about the Plugin this Block is an instance of. + * Not the best name - not all blocks come from plugins (ie Graph) + */ + virtual const PluginImpl* plugin_impl() const; + + virtual void plugin(PluginImpl* pi) { _plugin = pi; } + + virtual void set_buffer_size(RunContext& context, + BufferFactory& bufs, + LV2_URID type, + uint32_t size); + + /** The Graph this Block belongs to. */ + inline GraphImpl* parent_graph() const { return (GraphImpl*)_parent; } + + uint32_t num_ports() const { return _ports ? _ports->size() : 0; } + virtual uint32_t polyphony() const { return _polyphony; } + + /** Mark used during graph compilation */ + enum class Mark { UNVISITED, VISITING, VISITED }; + Mark get_mark() const { return _mark; } + void set_mark(Mark m) { _mark = m; } + +protected: + PortImpl* nth_port_by_type(uint32_t n, bool input, PortType type); + + PluginImpl* _plugin; + MPtr<Ports> _ports; ///< Access in audio thread only + uint32_t _polyphony; + std::set<BlockImpl*> _providers; ///< Blocks connected to this one's input ports + std::set<BlockImpl*> _dependants; ///< Blocks this one's output ports are connected to + Mark _mark; ///< Mark for graph compilation algorithm + bool _polyphonic; + bool _activated; + bool _enabled; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BLOCKIMPL_HPP diff --git a/src/server/Broadcaster.cpp b/src/server/Broadcaster.cpp new file mode 100644 index 00000000..00fefddd --- /dev/null +++ b/src/server/Broadcaster.cpp @@ -0,0 +1,97 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <utility> + +#include "ingen/Interface.hpp" + +#include "Broadcaster.hpp" +#include "PluginImpl.hpp" +#include "BlockFactory.hpp" + +namespace Ingen { +namespace Server { + +Broadcaster::Broadcaster() + : _must_broadcast(false) + , _bundle_depth(0) +{} + +Broadcaster::~Broadcaster() +{ + std::lock_guard<std::mutex> lock(_clients_mutex); + _clients.clear(); + _broadcastees.clear(); +} + +/** Register a client to receive messages over the notification band. + */ +void +Broadcaster::register_client(SPtr<Interface> client) +{ + std::lock_guard<std::mutex> lock(_clients_mutex); + _clients.insert(client); +} + +/** Remove a client from the list of registered clients. + * + * @return true if client was found and removed. + */ +bool +Broadcaster::unregister_client(SPtr<Interface> client) +{ + std::lock_guard<std::mutex> lock(_clients_mutex); + const size_t erased = _clients.erase(client); + _broadcastees.erase(client); + return (erased > 0); +} + +void +Broadcaster::set_broadcast(SPtr<Interface> client, bool broadcast) +{ + if (broadcast) { + _broadcastees.insert(client); + } else { + _broadcastees.erase(client); + } + _must_broadcast.store(!_broadcastees.empty()); +} + +void +Broadcaster::send_plugins(const BlockFactory::Plugins& plugins) +{ + std::lock_guard<std::mutex> lock(_clients_mutex); + for (const auto& c : _clients) { + send_plugins_to(c.get(), plugins); + } +} + +void +Broadcaster::send_plugins_to(Interface* client, + const BlockFactory::Plugins& plugins) +{ + client->bundle_begin(); + + for (const auto& p : plugins) { + const PluginImpl* const plugin = p.second; + client->put(plugin->uri(), plugin->properties()); + } + + client->bundle_end(); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Broadcaster.hpp b/src/server/Broadcaster.hpp new file mode 100644 index 00000000..3981b265 --- /dev/null +++ b/src/server/Broadcaster.hpp @@ -0,0 +1,118 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BROADCASTER_HPP +#define INGEN_ENGINE_BROADCASTER_HPP + +#include <atomic> +#include <list> +#include <mutex> +#include <set> +#include <string> + +#include "ingen/Interface.hpp" +#include "ingen/types.hpp" + +#include "BlockFactory.hpp" + +namespace Ingen { +namespace Server { + +/** Broadcaster for all clients. + * + * This is an Interface that forwards all messages to all registered + * clients (for updating all clients on state changes in the engine). + * + * \ingroup engine + */ +class Broadcaster : public Interface +{ +public: + Broadcaster(); + ~Broadcaster(); + + void register_client(SPtr<Interface> client); + bool unregister_client(SPtr<Interface> client); + + void set_broadcast(SPtr<Interface> client, bool broadcast); + + /** Ignore a client when broadcasting. + * + * This is used to prevent feeding back updates to the client that + * initiated a property set in the first place. + */ + void set_ignore_client(SPtr<Interface> client) { _ignore_client = client; } + void clear_ignore_client() { _ignore_client.reset(); } + + /** Return true iff there are any clients with broadcasting enabled. + * + * This is used in the audio thread to decide whether or not notifications + * should be calculated and emitted. + */ + bool must_broadcast() const { return _must_broadcast; } + + /** A handle that represents a transfer of possibly several changes. + * + * This object going out of scope signifies the transfer is completed. + * This makes doing the right thing in recursive functions that send + * updates simple (e.g. Event::post_process()). + */ + class Transfer : public Raul::Noncopyable { + public: + explicit Transfer(Broadcaster& b) : broadcaster(b) { + if (++broadcaster._bundle_depth == 1) { + broadcaster.bundle_begin(); + } + } + ~Transfer() { + if (--broadcaster._bundle_depth == 0) { + broadcaster.bundle_end(); + } + } + Broadcaster& broadcaster; + }; + + void send_plugins(const BlockFactory::Plugins& plugins); + void send_plugins_to(Interface*, const BlockFactory::Plugins& plugins); + + void message(const Message& msg) override { + std::lock_guard<std::mutex> lock(_clients_mutex); + for (const auto& c : _clients) { + if (c != _ignore_client) { + c->message(msg); + } + } + } + + URI uri() const override { return URI("ingen:/broadcaster"); } + +private: + friend class Transfer; + + typedef std::set<SPtr<Interface>> Clients; + + std::mutex _clients_mutex; + Clients _clients; + std::set< SPtr<Interface> > _broadcastees; + std::atomic<bool> _must_broadcast; + unsigned _bundle_depth; + SPtr<Interface> _ignore_client; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BROADCASTER_HPP diff --git a/src/server/Buffer.cpp b/src/server/Buffer.cpp new file mode 100644 index 00000000..34867fa3 --- /dev/null +++ b/src/server/Buffer.cpp @@ -0,0 +1,468 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#define __STDC_LIMIT_MACROS 1 + +#include <cmath> +#include <cstdint> +#include <cstring> +#include <new> + +#ifdef __SSE__ +# include <xmmintrin.h> +#endif + +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen_config.h" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "ingen/Log.hpp" + +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "RunContext.hpp" + +namespace Ingen { +namespace Server { + +Buffer::Buffer(BufferFactory& bufs, + LV2_URID type, + LV2_URID value_type, + uint32_t capacity, + bool external, + void* buf) + : _factory(bufs) + , _next(nullptr) + , _buf(external ? nullptr : aligned_alloc(capacity)) + , _latest_event(0) + , _type(type) + , _value_type(value_type) + , _capacity(capacity) + , _refs(0) + , _external(external) +{ + if (!external && !_buf) { + bufs.engine().log().rt_error("Failed to allocate buffer\n"); + throw std::bad_alloc(); + } + + if (type != bufs.uris().atom_Sound) { + /* Audio buffers are not atoms, the buffer is the start of a float + array which is already silent since the buffer is zeroed. All other + buffers are atoms. */ + if (_buf) { + LV2_Atom* atom = get<LV2_Atom>(); + atom->size = capacity - sizeof(LV2_Atom); + atom->type = type; + + clear(); + } + + if (value_type && value_type != type) { + /* Buffer with a different value type. These buffers (probably + sequences) have a "value" that persists independently of the buffer + contents. This is used to represent things like a Sequence of + Float, which acts like an individual float (has a value), but the + buffer itself only transmits changes and does not necessarily + contain the current value. */ + _value_buffer = bufs.get_buffer(value_type, 0, 0); + } + } +} + +Buffer::~Buffer() +{ + if (!_external) { + free(_buf); + } +} + +void +Buffer::recycle() +{ + _factory.recycle(this); +} + +void +Buffer::set_type(GetFn get, LV2_URID type, LV2_URID value_type) +{ + _type = type; + _value_type = value_type; + if (type == _factory.uris().atom_Sequence && value_type) { + _value_buffer = (_factory.*get)(value_type, 0, 0); + } +} + +void +Buffer::clear() +{ + if (is_audio() && _buf) { + memset(_buf, 0, _capacity); + } else if (is_control()) { + get<LV2_Atom_Float>()->body = 0; + } else if (is_sequence()) { + LV2_Atom_Sequence* seq = get<LV2_Atom_Sequence>(); + seq->atom.type = _factory.uris().atom_Sequence; + seq->atom.size = sizeof(LV2_Atom_Sequence_Body); + seq->body.unit = 0; + seq->body.pad = 0; + _latest_event = 0; + } +} + +void +Buffer::render_sequence(const RunContext& context, const Buffer* src, bool add) +{ + const LV2_URID atom_Float = _factory.uris().atom_Float; + const LV2_Atom_Sequence* seq = src->get<const LV2_Atom_Sequence>(); + const LV2_Atom_Float* init = (const LV2_Atom_Float*)src->value(); + float value = init ? init->body : 0.0f; + SampleCount offset = context.offset(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->time.frames >= offset && ev->body.type == atom_Float) { + write_block(value, offset, ev->time.frames, add); + value = ((const LV2_Atom_Float*)&ev->body)->body; + offset = ev->time.frames; + } + } + write_block(value, offset, context.offset() + context.nframes(), add); +} + +void +Buffer::copy(const RunContext& context, const Buffer* src) +{ + if (!_buf) { + return; + } else if (_type == src->type()) { + const uint32_t src_size = src->size(); + if (src_size <= _capacity) { + memcpy(_buf, src->_buf, src_size); + } else { + clear(); + } + } else if (src->is_audio() && is_control()) { + samples()[0] = src->samples()[0]; + } else if (src->is_control() && is_audio()) { + set_block(src->samples()[0], 0, context.nframes()); + } else if (src->is_sequence() && is_audio() && + src->value_type() == _factory.uris().atom_Float) { + render_sequence(context, src, false); + } else { + clear(); + } +} + +void +Buffer::resize(uint32_t capacity) +{ + if (!_external) { + _buf = realloc(_buf, capacity); + _capacity = capacity; + clear(); + } else { + _factory.engine().log().error("Attempt to resize external buffer\n"); + } +} + +void* +Buffer::port_data(PortType port_type, SampleCount offset) +{ + switch (port_type.id()) { + case PortType::ID::CONTROL: + return &_value_buffer->get<LV2_Atom_Float>()->body; + case PortType::ID::CV: + case PortType::ID::AUDIO: + if (_type == _factory.uris().atom_Float) { + return &get<LV2_Atom_Float>()->body; + } else if (_type == _factory.uris().atom_Sound) { + return (Sample*)_buf + offset; + } + break; + case PortType::ID::ATOM: + if (_type != _factory.uris().atom_Sound) { + return _buf; + } + default: break; + } + return nullptr; +} + +const void* +Buffer::port_data(PortType port_type, SampleCount offset) const +{ + return const_cast<void*>( + const_cast<Buffer*>(this)->port_data(port_type, offset)); +} + +#ifdef __SSE__ +/** Vector fabsf */ +static inline __m128 +mm_abs_ps(__m128 x) +{ + const __m128 sign_mask = _mm_set1_ps(-0.0f); // -0.0f = 1 << 31 + return _mm_andnot_ps(sign_mask, x); +} +#endif + +float +Buffer::peak(const RunContext& context) const +{ +#ifdef __SSE__ + const __m128* const vbuf = (const __m128*)samples(); + __m128 vpeak = mm_abs_ps(vbuf[0]); + const SampleCount nblocks = context.nframes() / 4; + + // First, find the vector absolute max of the buffer + for (SampleCount i = 1; i < nblocks; ++i) { + vpeak = _mm_max_ps(vpeak, mm_abs_ps(vbuf[i])); + } + + // Now we need the single max of vpeak + // vpeak = ABCD + // tmp = CDAB + __m128 tmp = _mm_shuffle_ps(vpeak, vpeak, _MM_SHUFFLE(2, 3, 0, 1)); + + // vpeak = MAX(A,C) MAX(B,D) MAX(C,A) MAX(D,B) + vpeak = _mm_max_ps(vpeak, tmp); + + // tmp = BADC of the new vpeak + // tmp = MAX(B,D) MAX(A,C) MAX(D,B) MAX(C,A) + tmp = _mm_shuffle_ps(vpeak, vpeak, _MM_SHUFFLE(1, 0, 3, 2)); + + // vpeak = MAX(MAX(A,C), MAX(B,D)), ... + vpeak = _mm_max_ps(vpeak, tmp); + + // peak = vpeak[0] + float peak; + _mm_store_ss(&peak, vpeak); + + return peak; +#else + const Sample* const buf = samples(); + float peak = 0.0f; + for (SampleCount i = 0; i < context.nframes(); ++i) { + peak = fmaxf(peak, fabsf(buf[i])); + } + return peak; +#endif +} + +void +Buffer::prepare_write(RunContext& context) +{ + if (_type == _factory.uris().atom_Sequence) { + LV2_Atom* atom = get<LV2_Atom>(); + + atom->type = (LV2_URID)_factory.uris().atom_Sequence; + atom->size = sizeof(LV2_Atom_Sequence_Body); + _latest_event = 0; + } +} + +void +Buffer::prepare_output_write(RunContext& context) +{ + if (_type == _factory.uris().atom_Sequence) { + LV2_Atom* atom = get<LV2_Atom>(); + + atom->type = (LV2_URID)_factory.uris().atom_Chunk; + atom->size = _capacity - sizeof(LV2_Atom); + _latest_event = 0; + } +} + +bool +Buffer::append_event(int64_t frames, + uint32_t size, + uint32_t type, + const uint8_t* data) +{ + assert(frames >= _latest_event); + + LV2_Atom* atom = get<LV2_Atom>(); + if (atom->type == _factory.uris().atom_Chunk) { + clear(); // Chunk initialized with prepare_output_write(), clear + } + + if (sizeof(LV2_Atom) + atom->size + lv2_atom_pad_size(size) > _capacity) { + return false; + } + + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)atom; + LV2_Atom_Event* ev = (LV2_Atom_Event*)( + (uint8_t*)seq + lv2_atom_total_size(&seq->atom)); + + ev->time.frames = frames; + ev->body.size = size; + ev->body.type = type; + memcpy(ev + 1, data, size); + + atom->size += sizeof(LV2_Atom_Event) + lv2_atom_pad_size(size); + + _latest_event = frames; + + return true; +} + +bool +Buffer::append_event(int64_t frames, const LV2_Atom* body) +{ + return append_event(frames, body->size, body->type, (const uint8_t*)(body + 1)); +} + +bool +Buffer::append_event_buffer(const Buffer* buf) +{ + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)get<LV2_Atom>(); + LV2_Atom_Sequence* bseq = (LV2_Atom_Sequence*)buf->get<LV2_Atom>(); + if (seq->atom.type == _factory.uris().atom_Chunk) { + clear(); // Chunk initialized with prepare_output_write(), clear + } + + const uint32_t total_size = lv2_atom_total_size(&seq->atom); + uint8_t* const end = (uint8_t*)seq + total_size; + const uint32_t n_bytes = bseq->atom.size - sizeof(bseq->body); + if (sizeof(LV2_Atom) + total_size + n_bytes >= _capacity) { + return false; // Not enough space + } + + memcpy(end, bseq + 1, n_bytes); + seq->atom.size += n_bytes; + + _latest_event = std::max(_latest_event, buf->_latest_event); + + return true; +} + +SampleCount +Buffer::next_value_offset(SampleCount offset, SampleCount end) const +{ + if (_type == _factory.uris().atom_Sequence && _value_type) { + const LV2_Atom_Sequence* seq = get<const LV2_Atom_Sequence>(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->time.frames > offset && + ev->time.frames < end && + ev->body.type == _value_type) { + return ev->time.frames; + } + } + } + + /* For CV buffers, it's possible to scan for a value change here, which for + stepped CV would do the right thing, but in the worst case (e.g. with + sine waves), when connected to a control port would split the cycle for + every frame which isn't feasible. Instead, just return end, so the + cycle will not be split. + + A plugin that takes CV and emits discrete change events, possibly with a + maximum rate or fuzz factor, would allow the user to choose which + behaviour, at the cost of some overhead. + */ + + return end; +} + +const LV2_Atom* +Buffer::value() const +{ + return _value_buffer ? _value_buffer->get<const LV2_Atom>() : nullptr; +} + +void +Buffer::set_value(const Atom& value) +{ + if (!value.is_valid() || !_value_buffer) { + return; + } + + const uint32_t total_size = sizeof(LV2_Atom) + value.size(); + if (total_size > _value_buffer->capacity()) { + _value_buffer = _factory.claim_buffer(value.type(), 0, total_size); + } + + memcpy(_value_buffer->get<LV2_Atom*>(), value.atom(), total_size); +} + +void +Buffer::update_value_buffer(SampleCount offset) +{ + if (!_value_buffer || !_value_type) { + return; + } + + LV2_Atom_Sequence* seq = get<LV2_Atom_Sequence>(); + LV2_Atom_Event* latest = nullptr; + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->time.frames > offset) { + break; + } else if (ev->body.type == _value_type) { + latest = ev; + } + } + + if (latest) { + memcpy(_value_buffer->get<LV2_Atom>(), + &latest->body, + lv2_atom_total_size(&latest->body)); + } +} + +#ifndef NDEBUG +void +Buffer::dump_cv(const RunContext& context) const +{ + float value = samples()[0]; + fprintf(stderr, "{ 0000: %.02f\n", value); + for (uint32_t i = 0; i < context.nframes(); ++i) { + if (samples()[i] != value) { + value = samples()[i]; + fprintf(stderr, " %4d: %.02f\n", i, value); + } + } + fprintf(stderr, "}\n"); +} +#endif + +void* Buffer::aligned_alloc(size_t size) +{ +#ifdef HAVE_POSIX_MEMALIGN + void* buf; + if (!posix_memalign((void**)&buf, 16, size)) { + memset(buf, 0, size); + return buf; + } +#else + return (LV2_buf*)calloc(1, size); +#endif + return nullptr; +} + +void +intrusive_ptr_add_ref(Buffer* b) +{ + b->ref(); +} + +void +intrusive_ptr_release(Buffer* b) +{ + b->deref(); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Buffer.hpp b/src/server/Buffer.hpp new file mode 100644 index 00000000..a95fcd3c --- /dev/null +++ b/src/server/Buffer.hpp @@ -0,0 +1,244 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BUFFER_HPP +#define INGEN_ENGINE_BUFFER_HPP + +#include <atomic> +#include <cassert> + +#include "ingen/types.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "raul/Deletable.hpp" + +#include "BufferFactory.hpp" +#include "PortType.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class BufferFactory; +class Engine; +class RunContext; + +class INGEN_API Buffer +{ +public: + Buffer(BufferFactory& bufs, + LV2_URID type, + LV2_URID value_type, + uint32_t capacity, + bool external = false, + void* buf = nullptr); + + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + + void clear(); + void resize(uint32_t capacity); + void copy(const RunContext& context, const Buffer* src); + void prepare_write(RunContext& context); + + void* port_data(PortType port_type, SampleCount offset); + const void* port_data(PortType port_type, SampleCount offset) const; + + inline LV2_URID type() const { return _type; } + inline LV2_URID value_type() const { return _value_type; } + inline uint32_t capacity() const { return _capacity; } + inline uint32_t size() const { + return is_audio() ? _capacity : sizeof(LV2_Atom) + get<LV2_Atom>()->size; + } + + typedef BufferRef (BufferFactory::*GetFn)(LV2_URID, LV2_URID, uint32_t); + + /** Set the buffer type and optional value type for this buffer. + * + * @param get Called to get auxiliary buffers if necessary. + * @param type Type of buffer. + * @param value_type Type of values in buffer if applicable (for sequences). + */ + void set_type(GetFn get, LV2_URID type, LV2_URID value_type); + + inline bool is_audio() const { + return _type == _factory.uris().atom_Sound; + } + + inline bool is_control() const { + return _type == _factory.uris().atom_Float; + } + + inline bool is_sequence() const { + return _type == _factory.uris().atom_Sequence; + } + + /// Audio or float buffers only + inline const Sample* samples() const { + if (is_control()) { + return (const Sample*)LV2_ATOM_BODY_CONST(get<LV2_Atom_Float>()); + } else if (is_audio()) { + return (const Sample*)_buf; + } + return nullptr; + } + + /// Audio buffers only + inline Sample* samples() { + if (is_control()) { + return (Sample*)LV2_ATOM_BODY(get<LV2_Atom_Float>()); + } else if (is_audio()) { + return (Sample*)_buf; + } + return nullptr; + } + + /// Numeric buffers only + inline Sample value_at(SampleCount offset) const { + if (is_audio() || is_control()) { + return samples()[offset]; + } else if (_value_buffer) { + return ((LV2_Atom_Float*)value())->body; + } + return 0.0f; + } + + inline void set_block(const Sample val, + const SampleCount start, + const SampleCount end) + { + if (is_sequence()) { + append_event(start, sizeof(val), _factory.uris().atom_Float, + reinterpret_cast<const uint8_t*>( + static_cast<const float*>(&val))); + _value_buffer->get<LV2_Atom_Float>()->body = val; + return; + } + + assert(is_audio() || is_control()); + assert(end <= _capacity / sizeof(Sample)); + // Note: Do not change this without ensuring GCC can still vectorize it + Sample* const buf = samples() + start; + for (SampleCount i = 0; i < (end - start); ++i) { + buf[i] = val; + } + } + + inline void add_block(const Sample val, + const SampleCount start, + const SampleCount end) + { + assert(is_audio() || is_control()); + assert(end <= _capacity / sizeof(Sample)); + // Note: Do not change this without ensuring GCC can still vectorize it + Sample* const buf = samples() + start; + for (SampleCount i = 0; i < (end - start); ++i) { + buf[i] += val; + } + } + + inline void write_block(const Sample val, + const SampleCount start, + const SampleCount end, + const bool add) + { + if (add) { + add_block(val, start, end); + } else { + set_block(val, start, end); + } + } + + /// Audio buffers only + float peak(const RunContext& context) const; + + /// Sequence buffers only + void prepare_output_write(RunContext& context); + + /// Sequence buffers only + bool append_event(int64_t frames, + uint32_t size, + uint32_t type, + const uint8_t* data); + + /// Sequence buffers only + bool append_event(int64_t frames, const LV2_Atom* body); + + /// Sequence buffers only + bool append_event_buffer(const Buffer* buf); + + /// Value buffer for numeric sequences + BufferRef value_buffer() { return _value_buffer; } + + /// Return the current value + const LV2_Atom* value() const; + + /// Set/initialise current value in value buffer + void set_value(const Atom& value); + + /// Return offset of the first value change after `offset` + SampleCount next_value_offset(SampleCount offset, SampleCount end) const; + + /// Update value buffer to value as of offset + void update_value_buffer(SampleCount offset); + + /// Set/add to audio buffer from the Sequence of Float in `src` + void render_sequence(const RunContext& context, const Buffer* src, bool add); + +#ifndef NDEBUG + void dump_cv(const RunContext& context) const; +#endif + + void set_capacity(uint32_t capacity) { _capacity = capacity; } + + void set_buffer(void* buf) { assert(_external); _buf = buf; } + + static void* aligned_alloc(size_t size); + + template<typename T> const T* get() const { return reinterpret_cast<const T*>(_buf); } + template<typename T> T* get() { return reinterpret_cast<T*>(_buf); } + + inline void ref() { ++_refs; } + + inline void deref() { + if ((--_refs) == 0) { + recycle(); + } + } + +private: + friend class BufferFactory; + ~Buffer(); + + void recycle(); + + BufferFactory& _factory; + Buffer* _next; ///< Intrusive linked list for BufferFactory + void* _buf; ///< Actual buffer memory + BufferRef _value_buffer; ///< Value buffer for numeric sequences + int64_t _latest_event; + LV2_URID _type; + LV2_URID _value_type; + uint32_t _capacity; + std::atomic<unsigned> _refs; ///< Intrusive reference count + bool _external; ///< Buffer is externally allocated +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BUFFER_HPP diff --git a/src/server/BufferFactory.cpp b/src/server/BufferFactory.cpp new file mode 100644 index 00000000..d5d947d0 --- /dev/null +++ b/src/server/BufferFactory.cpp @@ -0,0 +1,190 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" + +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" + +namespace Ingen { +namespace Server { + +BufferFactory::BufferFactory(Engine& engine, URIs& uris) + : _free_audio(nullptr) + , _free_control(nullptr) + , _free_sequence(nullptr) + , _free_object(nullptr) + , _engine(engine) + , _uris(uris) + , _seq_size(0) + , _silent_buffer(nullptr) +{ +} + +BufferFactory::~BufferFactory() +{ + _silent_buffer.reset(); + free_list(_free_audio.load()); + free_list(_free_control.load()); + free_list(_free_sequence.load()); + free_list(_free_object.load()); +} + +Forge& +BufferFactory::forge() +{ + return _engine.world()->forge(); +} + +Raul::Maid& +BufferFactory::maid() +{ + return *_engine.maid(); +} + +void +BufferFactory::free_list(Buffer* head) +{ + while (head) { + Buffer* next = head->_next; + delete head; + head = next; + } +} + +void +BufferFactory::set_block_length(SampleCount block_length) +{ + _silent_buffer = create(_uris.atom_Sound, audio_buffer_size(block_length)); +} + +uint32_t +BufferFactory::audio_buffer_size(SampleCount nframes) +{ + return nframes * sizeof(Sample); +} + +uint32_t +BufferFactory::audio_buffer_size() const +{ + return _engine.block_length() * sizeof(Sample); +} + +uint32_t +BufferFactory::default_size(LV2_URID type) const +{ + if (type == _uris.atom_Float) { + return sizeof(LV2_Atom_Float); + } else if (type == _uris.atom_Sound) { + return audio_buffer_size(_engine.block_length()); + } else if (type == _uris.atom_URID) { + return sizeof(LV2_Atom_URID); + } else if (type == _uris.atom_Sequence) { + if (_seq_size == 0) { + return _engine.sequence_size(); + } else { + return _seq_size; + } + } else { + return 0; + } +} + +Buffer* +BufferFactory::try_get_buffer(LV2_URID type) +{ + std::atomic<Buffer*>& head_ptr = free_list(type); + Buffer* head = nullptr; + Buffer* next; + do { + head = head_ptr.load(); + if (!head) { + break; + } + next = head->_next; + } while (!head_ptr.compare_exchange_weak(head, next)); + + return head; +} + +BufferRef +BufferFactory::get_buffer(LV2_URID type, + LV2_URID value_type, + uint32_t capacity) +{ + Buffer* try_head = try_get_buffer(type); + if (!try_head) { + return create(type, value_type, capacity); + } + + try_head->_next = nullptr; + try_head->set_type(&BufferFactory::get_buffer, type, value_type); + try_head->clear(); + return BufferRef(try_head); +} + +BufferRef +BufferFactory::claim_buffer(LV2_URID type, + LV2_URID value_type, + uint32_t capacity) +{ + Buffer* try_head = try_get_buffer(type); + if (!try_head) { + _engine.world()->log().rt_error("Failed to obtain buffer"); + return BufferRef(); + } + + try_head->_next = nullptr; + try_head->set_type(&BufferFactory::claim_buffer, type, value_type); + return BufferRef(try_head); +} + +BufferRef +BufferFactory::silent_buffer() +{ + return _silent_buffer; +} + +BufferRef +BufferFactory::create(LV2_URID type, LV2_URID value_type, uint32_t capacity) +{ + if (capacity == 0) { + capacity = default_size(type); + } else if (type == _uris.atom_Float) { + capacity = std::max(capacity, (uint32_t)sizeof(LV2_Atom_Float)); + } else if (type == _uris.atom_Sound) { + capacity = std::max(capacity, default_size(_uris.atom_Sound)); + } + + return BufferRef(new Buffer(*this, type, value_type, capacity)); +} + +void +BufferFactory::recycle(Buffer* buf) +{ + std::atomic<Buffer*>& head_ptr = free_list(buf->type()); + Buffer* try_head; + do { + try_head = head_ptr.load(); + buf->_next = try_head; + } while (!head_ptr.compare_exchange_weak(try_head, buf)); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/BufferFactory.hpp b/src/server/BufferFactory.hpp new file mode 100644 index 00000000..8265fc98 --- /dev/null +++ b/src/server/BufferFactory.hpp @@ -0,0 +1,118 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BUFFERFACTORY_HPP +#define INGEN_ENGINE_BUFFERFACTORY_HPP + +#include <atomic> +#include <map> +#include <mutex> + +#include "ingen/Atom.hpp" +#include "ingen/Forge.hpp" +#include "ingen/URIs.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" +#include "raul/RingBuffer.hpp" + +#include "BufferRef.hpp" +#include "PortType.hpp" +#include "types.hpp" + +namespace Raul { class Maid; } + +namespace Ingen { + +class URIs; + +namespace Server { + +class Engine; + +class INGEN_API BufferFactory { +public: + BufferFactory(Engine& engine, URIs& uris); + ~BufferFactory(); + + static uint32_t audio_buffer_size(SampleCount nframes); + + uint32_t audio_buffer_size() const; + uint32_t default_size(LV2_URID type) const; + + /** Dynamically allocate a new Buffer. */ + BufferRef create(LV2_URID type, + LV2_URID value_type, + uint32_t capacity = 0); + + /** Get a new buffer, reusing if possible, allocating if otherwise. */ + BufferRef get_buffer(LV2_URID type, + LV2_URID value_type, + uint32_t capacity); + + /** Claim an existing buffer, never allocates, real-time safe. */ + BufferRef claim_buffer(LV2_URID type, + LV2_URID value_type, + uint32_t capacity); + + /** Return a reference to a shared silent buffer. */ + BufferRef silent_buffer(); + + void set_block_length(SampleCount block_length); + void set_seq_size(uint32_t seq_size) { _seq_size = seq_size; } + + Forge& forge(); + Raul::Maid& maid(); + + URIs& uris() { return _uris; } + Engine& engine() { return _engine; } + +private: + friend class Buffer; + void recycle(Buffer* buf); + + Buffer* try_get_buffer(LV2_URID type); + + inline std::atomic<Buffer*>& free_list(LV2_URID type) { + if (type == _uris.atom_Float) { + return _free_control; + } else if (type == _uris.atom_Sound) { + return _free_audio; + } else if (type == _uris.atom_Sequence) { + return _free_sequence; + } else { + return _free_object; + } + } + + void free_list(Buffer* head); + + std::atomic<Buffer*> _free_audio; + std::atomic<Buffer*> _free_control; + std::atomic<Buffer*> _free_sequence; + std::atomic<Buffer*> _free_object; + + std::mutex _mutex; + Engine& _engine; + URIs& _uris; + uint32_t _seq_size; + + BufferRef _silent_buffer; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BUFFERFACTORY_HPP diff --git a/src/server/BufferRef.hpp b/src/server/BufferRef.hpp new file mode 100644 index 00000000..2a1cbc27 --- /dev/null +++ b/src/server/BufferRef.hpp @@ -0,0 +1,38 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_BUFFER_REF_HPP +#define INGEN_ENGINE_BUFFER_REF_HPP + +#include <boost/intrusive_ptr.hpp> + +#include "ingen/ingen.h" + +namespace Ingen { +namespace Server { + +class Buffer; + +typedef boost::intrusive_ptr<Buffer> BufferRef; + +// Defined in Buffer.cpp +INGEN_API void intrusive_ptr_add_ref(Buffer* b); +INGEN_API void intrusive_ptr_release(Buffer* b); + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BUFFER_REF_HPP diff --git a/src/server/ClientUpdate.cpp b/src/server/ClientUpdate.cpp new file mode 100644 index 00000000..60dd02e3 --- /dev/null +++ b/src/server/ClientUpdate.cpp @@ -0,0 +1,155 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Interface.hpp" +#include "ingen/URIs.hpp" + +#include "BlockImpl.hpp" +#include "BufferFactory.hpp" +#include "ClientUpdate.hpp" +#include "GraphImpl.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +void +ClientUpdate::put(const URI& uri, + const Properties& props, + Resource::Graph ctx) +{ + const ClientUpdate::Put put = { uri, props, ctx }; + puts.push_back(put); +} + +void +ClientUpdate::put_port(const PortImpl* port) +{ + const URIs& uris = port->bufs().uris(); + if (port->is_a(PortType::CONTROL) || port->is_a(PortType::CV)) { + Properties props = port->properties(); + props.erase(uris.ingen_value); + props.emplace(uris.ingen_value, port->value()); + put(port->uri(), props); + } else { + put(port->uri(), port->properties()); + } +} + +void +ClientUpdate::put_block(const BlockImpl* block) +{ + const PluginImpl* const plugin = block->plugin_impl(); + const URIs& uris = plugin->uris(); + + if (uris.ingen_Graph == plugin->type()) { + put_graph((const GraphImpl*)block); + } else { + put(block->uri(), block->properties()); + for (size_t j = 0; j < block->num_ports(); ++j) { + put_port(block->port_impl(j)); + } + } +} + +void +ClientUpdate::put_graph(const GraphImpl* graph) +{ + put(graph->uri(), + graph->properties(Resource::Graph::INTERNAL), + Resource::Graph::INTERNAL); + + put(graph->uri(), + graph->properties(Resource::Graph::EXTERNAL), + Resource::Graph::EXTERNAL); + + // Enqueue blocks + for (const auto& b : graph->blocks()) { + put_block(&b); + } + + // Enqueue ports + for (uint32_t i = 0; i < graph->num_ports_non_rt(); ++i) { + put_port(graph->port_impl(i)); + } + + // Enqueue arcs + for (const auto& a : graph->arcs()) { + const SPtr<const Arc> arc = a.second; + const Connect connect = { arc->tail_path(), arc->head_path() }; + connects.push_back(connect); + } +} + +void +ClientUpdate::put_plugin(PluginImpl* plugin) +{ + put(plugin->uri(), plugin->properties()); + + for (const auto& p : plugin->presets()) { + put_preset(plugin->uris(), plugin->uri(), p.first, p.second); + } +} + +void +ClientUpdate::put_preset(const URIs& uris, + const URI& plugin, + const URI& preset, + const std::string& label) +{ + const Properties props{ + { uris.rdf_type, uris.pset_Preset.urid }, + { uris.rdfs_label, uris.forge.alloc(label) }, + { uris.lv2_appliesTo, uris.forge.make_urid(plugin) }}; + put(preset, props); +} + +void +ClientUpdate::del(const URI& subject) +{ + dels.push_back(subject); +} + +/** Returns true if a is closer to the root than b. */ +static inline bool +put_higher_than(const ClientUpdate::Put& a, const ClientUpdate::Put& b) +{ + return (std::count(a.uri.begin(), a.uri.end(), '/') < + std::count(b.uri.begin(), b.uri.end(), '/')); +} + +void +ClientUpdate::send(Interface& dest) +{ + // Send deletions + for (const URI& subject : dels) { + dest.del(subject); + } + + // Send puts in increasing depth order so parents are sent first + std::stable_sort(puts.begin(), puts.end(), put_higher_than); + for (const ClientUpdate::Put& put : puts) { + dest.put(put.uri, put.properties, put.ctx); + } + + // Send connections + for (const ClientUpdate::Connect& connect : connects) { + dest.connect(connect.tail, connect.head); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/ClientUpdate.hpp b/src/server/ClientUpdate.hpp new file mode 100644 index 00000000..f1a361f7 --- /dev/null +++ b/src/server/ClientUpdate.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_CLIENTUPDATE_HPP +#define INGEN_ENGINE_CLIENTUPDATE_HPP + +#include <string> +#include <vector> + +#include "ingen/Resource.hpp" +#include "raul/Path.hpp" + +namespace Ingen { + +class Interface; +class URIs; + +namespace Server { + +class PortImpl; +class BlockImpl; +class GraphImpl; +class PluginImpl; + +/** A sequence of puts/connects/deletes to update clients. + * + * Events like Get construct this in pre_process() and later send it in + * post_process() to avoid the need to lock. + */ +struct ClientUpdate { + void put(const URI& uri, + const Properties& props, + Resource::Graph ctx = Resource::Graph::DEFAULT); + + void put_port(const PortImpl* port); + void put_block(const BlockImpl* block); + void put_graph(const GraphImpl* graph); + void put_plugin(PluginImpl* plugin); + void put_preset(const URIs& uris, + const URI& plugin, + const URI& preset, + const std::string& label); + + void del(const URI& subject); + + void send(Interface& dest); + + struct Put { + URI uri; + Properties properties; + Resource::Graph ctx; + }; + + struct Connect { + Raul::Path tail; + Raul::Path head; + }; + + std::vector<URI> dels; + std::vector<Put> puts; + std::vector<Connect> connects; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_CLIENTUPDATE_HPP diff --git a/src/server/CompiledGraph.cpp b/src/server/CompiledGraph.cpp new file mode 100644 index 00000000..35b07935 --- /dev/null +++ b/src/server/CompiledGraph.cpp @@ -0,0 +1,274 @@ +/* + This file is part of Ingen. + Copyright 2015-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <algorithm> + +#include "ingen/ColorContext.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/World.hpp" + +#include "CompiledGraph.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +/** Graph contains ambiguous feedback with no delay nodes. */ +class FeedbackException : public std::exception { +public: + FeedbackException(const BlockImpl* node, const BlockImpl* root=nullptr) + : node(node) + , root(root) + {} + + const BlockImpl* node; + const BlockImpl* root; +}; + +static bool +has_provider_with_many_dependants(BlockImpl* n) +{ + for (BlockImpl* p : n->providers()) { + if (p->dependants().size() > 1) { + return true; + } + } + + return false; +} + +CompiledGraph::CompiledGraph(GraphImpl* graph) + : _master(std::unique_ptr<Task>(new Task(Task::Mode::SEQUENTIAL))) +{ + compile_graph(graph); +} + +MPtr<CompiledGraph> +CompiledGraph::compile(Raul::Maid& maid, GraphImpl& graph) +{ + try { + return maid.make_managed<CompiledGraph>(&graph); + } catch (const FeedbackException& e) { + Log& log = graph.engine().log(); + if (e.node && e.root) { + log.error(fmt("Feedback compiling %1% from %2%\n") + % e.node->path() % e.root->path()); + } else { + log.error(fmt("Feedback compiling %1%\n") + % e.node->path()); + } + return MPtr<CompiledGraph>(); + } +} + +static size_t +num_unvisited_dependants(BlockImpl* block) +{ + size_t count = 0; + for (BlockImpl* b : block->dependants()) { + if (b->get_mark() == BlockImpl::Mark::UNVISITED) { + ++count; + } + } + return count; +} + +static size_t +parallel_depth(BlockImpl* block) +{ + if (has_provider_with_many_dependants(block)) { + return 2; + } + + size_t min_provider_depth = std::numeric_limits<size_t>::max(); + for (auto p : block->providers()) { + min_provider_depth = std::min(min_provider_depth, parallel_depth(p)); + } + + return 2 + min_provider_depth; +} + +void +CompiledGraph::compile_graph(GraphImpl* graph) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + // Start with sink nodes (no outputs, or connected only to graph outputs) + std::set<BlockImpl*> blocks; + for (auto& b : graph->blocks()) { + // Mark all blocks as unvisited initially + b.set_mark(BlockImpl::Mark::UNVISITED); + + if (b.dependants().empty()) { + // Block has no dependants, add to initial working set + blocks.insert(&b); + } + } + + // Keep compiling working set until all nodes are visited + while (!blocks.empty()) { + std::set<BlockImpl*> predecessors; + + // Calculate maximum sequential depth to consume this phase + size_t depth = std::numeric_limits<size_t>::max(); + for (auto i : blocks) { + depth = std::min(depth, parallel_depth(i)); + } + + Task par(Task::Mode::PARALLEL); + for (auto b : blocks) { + assert(num_unvisited_dependants(b) == 0); + Task seq(Task::Mode::SEQUENTIAL); + compile_block(b, seq, depth, predecessors); + par.push_front(std::move(seq)); + } + _master->push_front(std::move(par)); + blocks = predecessors; + } + + _master = Task::simplify(std::move(_master)); + + if (graph->engine().world()->conf().option("trace").get<int32_t>()) { + ColorContext ctx(stderr, ColorContext::Color::YELLOW); + dump(graph->path()); + } +} + +/** Throw a FeedbackException iff `dependant` has `root` as a dependency. */ +static void +check_feedback(const BlockImpl* root, BlockImpl* provider) +{ + if (provider == root) { + throw FeedbackException(root); + } + + for (auto p : provider->providers()) { + const BlockImpl::Mark mark = p->get_mark(); + switch (mark) { + case BlockImpl::Mark::UNVISITED: + p->set_mark(BlockImpl::Mark::VISITING); + check_feedback(root, p); + break; + case BlockImpl::Mark::VISITING: + throw FeedbackException(p, root); + case BlockImpl::Mark::VISITED: + break; + } + p->set_mark(mark); + } +} + +void +CompiledGraph::compile_provider(const BlockImpl* root, + BlockImpl* block, + Task& task, + size_t max_depth, + std::set<BlockImpl*>& k) +{ + if (block->dependants().size() > 1) { + /* Provider has other dependants, so this is the tail of a sequential task. + Add provider to future working set and stop traversal. */ + check_feedback(root, block); + if (num_unvisited_dependants(block) == 0) { + k.insert(block); + } + } else if (max_depth > 0) { + // Calling dependant has only this provider, add here + if (task.mode() == Task::Mode::PARALLEL) { + // Inside a parallel task, compile into a new sequential child + Task seq(Task::Mode::SEQUENTIAL); + compile_block(block, seq, max_depth, k); + task.push_front(std::move(seq)); + } else { + // Prepend to given sequential task + compile_block(block, task, max_depth, k); + } + } else { + if (num_unvisited_dependants(block) == 0) { + k.insert(block); + } + } +} + +void +CompiledGraph::compile_block(BlockImpl* n, + Task& task, + size_t max_depth, + std::set<BlockImpl*>& k) +{ + switch (n->get_mark()) { + case BlockImpl::Mark::UNVISITED: + n->set_mark(BlockImpl::Mark::VISITING); + + // Execute this task after the providers to follow + task.push_front(Task(Task::Mode::SINGLE, n)); + + if (n->providers().size() < 2) { + // Single provider, prepend it to this sequential task + for (auto p : n->providers()) { + compile_provider(n, p, task, max_depth - 1, k); + } + } else if (has_provider_with_many_dependants(n)) { + // Stop recursion and enqueue providers for the next round + for (auto p : n->providers()) { + if (num_unvisited_dependants(p) == 0) { + k.insert(p); + } + } + } else { + // Multiple providers with only this node as dependant, + // make a new parallel task to execute them + Task par(Task::Mode::PARALLEL); + for (auto p : n->providers()) { + compile_provider(n, p, par, max_depth - 1, k); + } + task.push_front(std::move(par)); + } + n->set_mark(BlockImpl::Mark::VISITED); + break; + + case BlockImpl::Mark::VISITING: + throw FeedbackException(n); + + case BlockImpl::Mark::VISITED: + break; + } +} + +void +CompiledGraph::run(RunContext& context) +{ + _master->run(context); +} + +void +CompiledGraph::dump(const std::string& name) const +{ + auto sink = [](const std::string& s) { + fwrite(s.c_str(), 1, s.size(), stderr); + }; + + sink("(compiled-graph "); + sink(name); + _master->dump(sink, 2, false); + sink(")\n"); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/CompiledGraph.hpp b/src/server/CompiledGraph.hpp new file mode 100644 index 00000000..6b802611 --- /dev/null +++ b/src/server/CompiledGraph.hpp @@ -0,0 +1,84 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_COMPILEDGRAPH_HPP +#define INGEN_ENGINE_COMPILEDGRAPH_HPP + +#include <functional> +#include <set> +#include <vector> + +#include "ingen/types.hpp" +#include "raul/Maid.hpp" +#include "raul/Noncopyable.hpp" + +#include "Task.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; +class RunContext; + +/** A graph ``compiled'' into a quickly executable form. + * + * This is a flat sequence of nodes ordered such that the process thread can + * execute the nodes in order and have nodes always executed before any of + * their dependencies. + */ +class CompiledGraph : public Raul::Maid::Disposable + , public Raul::Noncopyable +{ +public: + static MPtr<CompiledGraph> compile(Raul::Maid& maid, GraphImpl& graph); + + void run(RunContext& context); + +private: + friend class Raul::Maid; ///< Allow make_managed to construct + + CompiledGraph(GraphImpl* graph); + + typedef std::set<BlockImpl*> BlockSet; + + void dump(const std::string& name) const; + + void compile_graph(GraphImpl* graph); + + void compile_block(BlockImpl* n, + Task& task, + size_t max_depth, + BlockSet& k); + + void compile_provider(const BlockImpl* root, + BlockImpl* block, + Task& task, + size_t max_depth, + BlockSet& k); + + std::unique_ptr<Task> _master; +}; + +inline MPtr<CompiledGraph> compile(Raul::Maid& maid, GraphImpl& graph) +{ + return CompiledGraph::compile(maid, graph); +} + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_COMPILEDGRAPH_HPP diff --git a/src/server/ControlBindings.cpp b/src/server/ControlBindings.cpp new file mode 100644 index 00000000..3901d1c2 --- /dev/null +++ b/src/server/ControlBindings.cpp @@ -0,0 +1,425 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "Buffer.hpp" +#include "ControlBindings.hpp" +#include "Engine.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +ControlBindings::ControlBindings(Engine& engine) + : _engine(engine) + , _learn_binding(nullptr) + , _bindings(new Bindings()) + , _feedback(new Buffer(*_engine.buffer_factory(), + engine.world()->uris().atom_Sequence, + 0, + 4096)) // FIXME: capacity? +{ + lv2_atom_forge_init( + &_forge, &engine.world()->uri_map().urid_map_feature()->urid_map); +} + +ControlBindings::~ControlBindings() +{ + _feedback.reset(); + delete _learn_binding.load(); +} + +ControlBindings::Key +ControlBindings::port_binding(PortImpl* port) const +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + const Ingen::URIs& uris = _engine.world()->uris(); + const Atom& binding = port->get_property(uris.midi_binding); + return binding_key(binding); +} + +ControlBindings::Key +ControlBindings::binding_key(const Atom& binding) const +{ + const Ingen::URIs& uris = _engine.world()->uris(); + Key key; + LV2_Atom* num = nullptr; + if (binding.type() == uris.atom_Object) { + const LV2_Atom_Object_Body* obj = (const LV2_Atom_Object_Body*) + binding.get_body(); + if (obj->otype == uris.midi_Bender) { + key = Key(Type::MIDI_BENDER); + } else if (obj->otype == uris.midi_ChannelPressure) { + key = Key(Type::MIDI_CHANNEL_PRESSURE); + } else if (obj->otype == uris.midi_Controller) { + lv2_atom_object_body_get( + binding.size(), obj, (LV2_URID)uris.midi_controllerNumber, &num, NULL); + if (!num) { + _engine.log().rt_error("Controller binding missing number\n"); + } else if (num->type != uris.atom_Int) { + _engine.log().rt_error("Controller number not an integer\n"); + } else { + key = Key(Type::MIDI_CC, ((LV2_Atom_Int*)num)->body); + } + } else if (obj->otype == uris.midi_NoteOn) { + lv2_atom_object_body_get( + binding.size(), obj, (LV2_URID)uris.midi_noteNumber, &num, NULL); + if (!num) { + _engine.log().rt_error("Note binding missing number\n"); + } else if (num->type != uris.atom_Int) { + _engine.log().rt_error("Note number not an integer\n"); + } else { + key = Key(Type::MIDI_NOTE, ((LV2_Atom_Int*)num)->body); + } + } + } else if (binding.type()) { + _engine.log().rt_error("Unknown binding type\n"); + } + return key; +} + +ControlBindings::Key +ControlBindings::midi_event_key(uint16_t size, const uint8_t* buf, uint16_t& value) +{ + switch (lv2_midi_message_type(buf)) { + case LV2_MIDI_MSG_CONTROLLER: + value = static_cast<int8_t>(buf[2]); + return Key(Type::MIDI_CC, static_cast<int8_t>(buf[1])); + case LV2_MIDI_MSG_BENDER: + value = (static_cast<int8_t>(buf[2]) << 7) + static_cast<int8_t>(buf[1]); + return Key(Type::MIDI_BENDER); + case LV2_MIDI_MSG_CHANNEL_PRESSURE: + value = static_cast<int8_t>(buf[1]); + return Key(Type::MIDI_CHANNEL_PRESSURE); + case LV2_MIDI_MSG_NOTE_ON: + value = 1.0f; + return Key(Type::MIDI_NOTE, static_cast<int8_t>(buf[1])); + default: + return Key(); + } +} + +bool +ControlBindings::set_port_binding(RunContext& context, + PortImpl* port, + Binding* binding, + const Atom& value) +{ + const Key key = binding_key(value); + if (!!key) { + binding->key = key; + binding->port = port; + _bindings->insert(*binding); + return true; + } else { + return false; + } +} + +void +ControlBindings::port_value_changed(RunContext& ctx, + PortImpl* port, + Key key, + const Atom& value_atom) +{ + Ingen::World* world = ctx.engine().world(); + const Ingen::URIs& uris = world->uris(); + if (!!key) { + int16_t value = port_value_to_control( + ctx, port, key.type, value_atom); + uint16_t size = 0; + uint8_t buf[4]; + switch (key.type) { + case Type::MIDI_CC: + size = 3; + buf[0] = LV2_MIDI_MSG_CONTROLLER; + buf[1] = key.num; + buf[2] = static_cast<int8_t>(value); + break; + case Type::MIDI_CHANNEL_PRESSURE: + size = 2; + buf[0] = LV2_MIDI_MSG_CHANNEL_PRESSURE; + buf[1] = static_cast<int8_t>(value); + break; + case Type::MIDI_BENDER: + size = 3; + buf[0] = LV2_MIDI_MSG_BENDER; + buf[1] = (value & 0x007F); + buf[2] = (value & 0x7F00) >> 7; + break; + case Type::MIDI_NOTE: + size = 3; + if (value == 1) { + buf[0] = LV2_MIDI_MSG_NOTE_ON; + } else if (value == 0) { + buf[0] = LV2_MIDI_MSG_NOTE_OFF; + } + buf[1] = key.num; + buf[2] = 0x64; // MIDI spec default + break; + default: + break; + } + if (size > 0) { + _feedback->append_event(ctx.nframes() - 1, size, (LV2_URID)uris.midi_MidiEvent, buf); + } + } +} + +void +ControlBindings::start_learn(PortImpl* port) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + Binding* b = _learn_binding.load(); + if (!b) { + _learn_binding = new Binding(Type::NULL_CONTROL, port); + } else { + b->port = port; + } +} + +static void +get_range(RunContext& context, const PortImpl* port, float* min, float* max) +{ + *min = port->minimum().get<float>(); + *max = port->maximum().get<float>(); + if (port->is_sample_rate()) { + *min *= context.engine().sample_rate(); + *max *= context.engine().sample_rate(); + } +} + +float +ControlBindings::control_to_port_value(RunContext& context, + const PortImpl* port, + Type type, + int16_t value) const +{ + float normal = 0.0f; + switch (type) { + case Type::MIDI_CC: + case Type::MIDI_CHANNEL_PRESSURE: + normal = (float)value / 127.0f; + break; + case Type::MIDI_BENDER: + normal = (float)value / 16383.0f; + break; + case Type::MIDI_NOTE: + normal = (value == 0.0f) ? 0.0f : 1.0f; + break; + default: + break; + } + + if (port->is_logarithmic()) { + normal = (expf(normal) - 1.0f) / ((float)M_E - 1.0f); + } + + float min, max; + get_range(context, port, &min, &max); + + return normal * (max - min) + min; +} + +int16_t +ControlBindings::port_value_to_control(RunContext& context, + PortImpl* port, + Type type, + const Atom& value_atom) const +{ + if (value_atom.type() != port->bufs().forge().Float) { + return 0; + } + + float min, max; + get_range(context, port, &min, &max); + + const float value = value_atom.get<float>(); + float normal = (value - min) / (max - min); + + if (normal < 0.0f) { + normal = 0.0f; + } + + if (normal > 1.0f) { + normal = 1.0f; + } + + if (port->is_logarithmic()) { + normal = logf(normal * ((float)M_E - 1.0f) + 1.0); + } + + switch (type) { + case Type::MIDI_CC: + case Type::MIDI_CHANNEL_PRESSURE: + return lrintf(normal * 127.0f); + case Type::MIDI_BENDER: + return lrintf(normal * 16383.0f); + case Type::MIDI_NOTE: + return (value > 0.0f) ? 1 : 0; + default: + return 0; + } +} + +static void +forge_binding(const URIs& uris, + LV2_Atom_Forge* forge, + ControlBindings::Type binding_type, + int32_t value) +{ + LV2_Atom_Forge_Frame frame; + switch (binding_type) { + case ControlBindings::Type::MIDI_CC: + lv2_atom_forge_object(forge, &frame, 0, uris.midi_Controller); + lv2_atom_forge_key(forge, uris.midi_controllerNumber); + lv2_atom_forge_int(forge, value); + break; + case ControlBindings::Type::MIDI_BENDER: + lv2_atom_forge_object(forge, &frame, 0, uris.midi_Bender); + break; + case ControlBindings::Type::MIDI_CHANNEL_PRESSURE: + lv2_atom_forge_object(forge, &frame, 0, uris.midi_ChannelPressure); + break; + case ControlBindings::Type::MIDI_NOTE: + lv2_atom_forge_object(forge, &frame, 0, uris.midi_NoteOn); + lv2_atom_forge_key(forge, uris.midi_noteNumber); + lv2_atom_forge_int(forge, value); + break; + case ControlBindings::Type::MIDI_RPN: // TODO + case ControlBindings::Type::MIDI_NRPN: // TODO + case ControlBindings::Type::NULL_CONTROL: + break; + } +} + +void +ControlBindings::set_port_value(RunContext& context, + PortImpl* port, + Type type, + int16_t value) +{ + float min, max; + get_range(context, port, &min, &max); + + const float val = control_to_port_value(context, port, type, value); + + // TODO: Set port value property so it is saved + port->set_control_value(context, context.start(), val); + + URIs& uris = context.engine().world()->uris(); + context.notify(uris.ingen_value, context.start(), port, + sizeof(float), _forge.Float, &val); +} + +bool +ControlBindings::finish_learn(RunContext& context, Key key) +{ + const Ingen::URIs& uris = context.engine().world()->uris(); + Binding* binding = _learn_binding.exchange(nullptr); + if (!binding || (key.type == Type::MIDI_NOTE && !binding->port->is_toggled())) { + return false; + } + + binding->key = key; + _bindings->insert(*binding); + + LV2_Atom buf[16]; + memset(buf, 0, sizeof(buf)); + lv2_atom_forge_set_buffer(&_forge, (uint8_t*)buf, sizeof(buf)); + forge_binding(uris, &_forge, key.type, key.num); + const LV2_Atom* atom = buf; + context.notify(uris.midi_binding, + context.start(), + binding->port, + atom->size, atom->type, LV2_ATOM_BODY_CONST(atom)); + + return true; +} + +void +ControlBindings::get_all(const Raul::Path& path, std::vector<Binding*>& bindings) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + for (Binding& b : *_bindings) { + if (b.port->path() == path || b.port->path().is_child_of(path)) { + bindings.push_back(&b); + } + } +} + +void +ControlBindings::remove(RunContext& ctx, const std::vector<Binding*>& bindings) +{ + for (Binding* b : bindings) { + _bindings->erase(*b); + } +} + +void +ControlBindings::pre_process(RunContext& ctx, Buffer* buffer) +{ + uint16_t value = 0; + Ingen::World* world = ctx.engine().world(); + const Ingen::URIs& uris = world->uris(); + + _feedback->clear(); + if ((!_learn_binding && _bindings->empty()) || !buffer->get<LV2_Atom>()) { + return; // Don't bother reading input + } + + LV2_Atom_Sequence* seq = buffer->get<LV2_Atom_Sequence>(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->body.type == uris.midi_MidiEvent) { + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY(&ev->body); + const Key key = midi_event_key(ev->body.size, buf, value); + + if (_learn_binding && !!key) { + finish_learn(ctx, key); // Learn new binding + } + + // Set all controls bound to this key + const Binding k = {key, nullptr}; + for (Bindings::const_iterator i = _bindings->lower_bound(k); + i != _bindings->end() && i->key == key; + ++i) { + set_port_value(ctx, i->port, key.type, value); + } + } + } +} + +void +ControlBindings::post_process(RunContext& context, Buffer* buffer) +{ + if (buffer->get<LV2_Atom>()) { + buffer->append_event_buffer(_feedback.get()); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/ControlBindings.hpp b/src/server/ControlBindings.hpp new file mode 100644 index 00000000..3160f8b2 --- /dev/null +++ b/src/server/ControlBindings.hpp @@ -0,0 +1,148 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_CONTROLBINDINGS_HPP +#define INGEN_ENGINE_CONTROLBINDINGS_HPP + +#include <atomic> +#include <cstdint> +#include <vector> + +#include <boost/intrusive/options.hpp> +#include <boost/intrusive/set.hpp> + +#include "ingen/Atom.hpp" +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "BufferFactory.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class RunContext; +class PortImpl; + +class ControlBindings { +public: + enum class Type : uint16_t { + NULL_CONTROL, + MIDI_BENDER, + MIDI_CC, + MIDI_RPN, + MIDI_NRPN, + MIDI_CHANNEL_PRESSURE, + MIDI_NOTE + }; + + struct Key { + Key(Type t=Type::NULL_CONTROL, int16_t n=0) : type(t), num(n) {} + inline bool operator<(const Key& other) const { + return ((type < other.type) || + (type == other.type && num < other.num)); + } + inline bool operator==(const Key& other) const { + return type == other.type && num == other.num; + } + inline bool operator!() const { return type == Type::NULL_CONTROL; } + Type type; + int16_t num; + }; + + /** One binding of a controller to a port. */ + struct Binding : public boost::intrusive::set_base_hook<>, + public Raul::Maid::Disposable { + Binding(Key k=Key(), PortImpl* p=nullptr) : key(std::move(k)), port(p) {} + + inline bool operator<(const Binding& rhs) const { return key < rhs.key; } + + Key key; + PortImpl* port; + }; + + /** Comparator for bindings by key. */ + struct BindingLess { + bool operator()(const Binding& lhs, const Binding& rhs) const { + return lhs.key < rhs.key; + } + }; + + explicit ControlBindings(Engine& engine); + ~ControlBindings(); + + Key port_binding(PortImpl* port) const; + Key binding_key(const Atom& binding) const; + + void start_learn(PortImpl* port); + + /** Set the binding for `port` to `binding` and take ownership of it. */ + bool set_port_binding(RunContext& ctx, + PortImpl* port, + Binding* binding, + const Atom& value); + + void port_value_changed(RunContext& ctx, + PortImpl* port, + Key key, + const Atom& value_atom); + + void pre_process(RunContext& ctx, Buffer* buffer); + void post_process(RunContext& ctx, Buffer* buffer); + + /** Get all bindings for `path` or children of `path`. */ + void get_all(const Raul::Path& path, std::vector<Binding*>& bindings); + + /** Remove a set of bindings from an earlier call to get_all(). */ + void remove(RunContext& ctx, const std::vector<Binding*>& bindings); + +private: + typedef boost::intrusive::multiset< + Binding, + boost::intrusive::compare<BindingLess> > Bindings; + + Key midi_event_key(uint16_t size, const uint8_t* buf, uint16_t& value); + + void set_port_value(RunContext& context, + PortImpl* port, + Type type, + int16_t value); + + bool finish_learn(RunContext& context, Key key); + + float control_to_port_value(RunContext& context, + const PortImpl* port, + Type type, + int16_t value) const; + + int16_t port_value_to_control(RunContext& context, + PortImpl* port, + Type type, + const Atom& value_atom) const; + + Engine& _engine; + std::atomic<Binding*> _learn_binding; + SPtr<Bindings> _bindings; + BufferRef _feedback; + LV2_Atom_Forge _forge; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_CONTROLBINDINGS_HPP diff --git a/src/server/DirectDriver.hpp b/src/server/DirectDriver.hpp new file mode 100644 index 00000000..58b4f898 --- /dev/null +++ b/src/server/DirectDriver.hpp @@ -0,0 +1,108 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_DIRECT_DRIVER_HPP +#define INGEN_ENGINE_DIRECT_DRIVER_HPP + +#include <boost/intrusive/slist.hpp> + +#include "Driver.hpp" +#include "Engine.hpp" + +namespace Ingen { +namespace Server { + +/** Driver for running Ingen directly as a library. + * \ingroup engine + */ +class DirectDriver : public Driver { +public: + DirectDriver(Engine& engine, + double sample_rate, + SampleCount block_length, + size_t seq_size) + : _engine(engine) + , _sample_rate(sample_rate) + , _block_length(block_length) + , _seq_size(seq_size) + {} + + virtual ~DirectDriver() { + _ports.clear_and_dispose([](EnginePort* p) { delete p; }); + } + + bool dynamic_ports() const { return true; } + + virtual EnginePort* create_port(DuplexPort* graph_port) { + return new EnginePort(graph_port); + } + + virtual EnginePort* get_port(const Raul::Path& path) { + for (auto& p : _ports) { + if (p.graph_port()->path() == path) { + return &p; + } + } + + return nullptr; + } + + virtual void add_port(RunContext& context, EnginePort* port) { + _ports.push_back(*port); + } + + virtual void remove_port(RunContext& context, EnginePort* port) { + _ports.erase(_ports.iterator_to(*port)); + } + + virtual void rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) {} + + virtual void port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) {} + + virtual void register_port(EnginePort& port) {} + virtual void unregister_port(EnginePort& port) {} + + virtual SampleCount block_length() const { return _block_length; } + + virtual size_t seq_size() const { return _seq_size; } + + virtual SampleCount sample_rate() const { return _sample_rate; } + + virtual SampleCount frame_time() const { return _engine.run_context().start(); } + + virtual void append_time_events(RunContext& context, Buffer& buffer) {} + + virtual int real_time_priority() { return 60; } + +private: + typedef boost::intrusive::slist<EnginePort, + boost::intrusive::cache_last<true> + > Ports; + + Engine& _engine; + Ports _ports; + SampleCount _sample_rate; + SampleCount _block_length; + size_t _seq_size; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_DIRECT_DRIVER_HPP diff --git a/src/server/Driver.hpp b/src/server/Driver.hpp new file mode 100644 index 00000000..9ae4b836 --- /dev/null +++ b/src/server/Driver.hpp @@ -0,0 +1,110 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_DRIVER_HPP +#define INGEN_ENGINE_DRIVER_HPP + +#include "raul/Noncopyable.hpp" + +#include "DuplexPort.hpp" +#include "EnginePort.hpp" + +namespace Raul { class Path; } + +namespace Ingen { +namespace Server { + +class DuplexPort; +class EnginePort; + +/** Engine driver base class. + * + * A Driver is responsible for managing system ports, and possibly running the + * audio graph. + * + * \ingroup engine + */ +class Driver : public Raul::Noncopyable { +public: + virtual ~Driver() = default; + + /** Activate driver (begin processing graph and events). */ + virtual bool activate() { return true; } + + /** Deactivate driver (stop processing graph and events). */ + virtual void deactivate() {} + + /** Create a port ready to be inserted with add_input (non realtime). + * May return NULL if the Driver can not create the port for some reason. + */ + virtual EnginePort* create_port(DuplexPort* graph_port) = 0; + + /** Find a system port by path. */ + virtual EnginePort* get_port(const Raul::Path& path) = 0; + + /** Add a system visible port (e.g. a port on the root graph). */ + virtual void add_port(RunContext& context, EnginePort* port) = 0; + + /** Remove a system visible port. + * + * This removes the port from the driver in the process thread but does not + * destroy the port. To actually remove the system port, unregister_port() + * must be called later in another thread. + */ + virtual void remove_port(RunContext& context, EnginePort* port) = 0; + + /** Return true iff driver supports dynamic adding/removing of ports. */ + virtual bool dynamic_ports() const { return false; } + + /** Register a system visible port. */ + virtual void register_port(EnginePort& port) = 0; + + /** Register a system visible port. */ + virtual void unregister_port(EnginePort& port) = 0; + + /** Rename a system visible port. */ + virtual void rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) = 0; + + /** Apply a system visible port property. */ + virtual void port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) = 0; + + /** Return the audio buffer size in frames */ + virtual SampleCount block_length() const = 0; + + /** Return the event buffer size in bytes */ + virtual size_t seq_size() const = 0; + + /** Return the sample rate in Hz */ + virtual SampleRate sample_rate() const = 0; + + /** Return the current frame time (running counter) */ + virtual SampleCount frame_time() const = 0; + + /** Append time events for this cycle to `buffer`. */ + virtual void append_time_events(RunContext& context, + Buffer& buffer) = 0; + + /** Return the real-time priority of the audio thread, or -1. */ + virtual int real_time_priority() = 0; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_DRIVER_HPP diff --git a/src/server/DuplexPort.cpp b/src/server/DuplexPort.cpp new file mode 100644 index 00000000..1b62ff38 --- /dev/null +++ b/src/server/DuplexPort.cpp @@ -0,0 +1,236 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" + +#include "Buffer.hpp" +#include "Driver.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" + +namespace Ingen { +namespace Server { + +DuplexPort::DuplexPort(BufferFactory& bufs, + GraphImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + bool polyphonic, + PortType type, + LV2_URID buf_type, + size_t buf_size, + const Atom& value, + bool is_output) + : InputPort(bufs, parent, symbol, index, parent->polyphony(), type, buf_type, value, buf_size) +{ + if (polyphonic) { + set_property(bufs.uris().ingen_polyphonic, bufs.forge().make(true)); + } + + if (!parent->parent() || + _poly != parent->parent_graph()->internal_poly()) { + _poly = 1; + } + + // Set default control range + if (!is_output && value.type() == bufs.uris().atom_Float) { + set_property(bufs.uris().lv2_minimum, bufs.forge().make(0.0f)); + set_property(bufs.uris().lv2_maximum, bufs.forge().make(1.0f)); + } + + _is_output = is_output; + if (is_output) { + if (parent->graph_type() != Node::GraphType::GRAPH) { + remove_property(bufs.uris().rdf_type, bufs.uris().lv2_InputPort.urid); + add_property(bufs.uris().rdf_type, bufs.uris().lv2_OutputPort.urid); + } + } + + get_buffers(bufs, &BufferFactory::get_buffer, + _voices, parent->polyphony(), 0); +} + +DuplexPort::~DuplexPort() +{ + if (is_linked()) { + parent_graph()->remove_port(*this); + } +} + +DuplexPort* +DuplexPort::duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) +{ + BufferFactory& bufs = *engine.buffer_factory(); + const Atom polyphonic = get_property(bufs.uris().ingen_polyphonic); + + DuplexPort* dup = new DuplexPort( + bufs, parent, symbol, _index, + polyphonic.type() == bufs.uris().atom_Bool && polyphonic.get<int32_t>(), + _type, _buffer_type, _buffer_size, + _value, _is_output); + + dup->set_properties(properties()); + + return dup; +} + +void +DuplexPort::inherit_neighbour(const PortImpl* port, + Properties& remove, + Properties& add) +{ + const URIs& uris = _bufs.uris(); + + /* TODO: This needs to become more sophisticated, and correct the situation + if the port is disconnected. */ + if (_type == PortType::CONTROL || _type == PortType::CV) { + if (port->minimum().get<float>() < _min.get<float>()) { + _min = port->minimum(); + remove.emplace(uris.lv2_minimum, uris.patch_wildcard); + add.emplace(uris.lv2_minimum, port->minimum()); + } + if (port->maximum().get<float>() > _max.get<float>()) { + _max = port->maximum(); + remove.emplace(uris.lv2_maximum, uris.patch_wildcard); + add.emplace(uris.lv2_maximum, port->maximum()); + } + } else if (_type == PortType::ATOM) { + for (auto i = port->properties().find(uris.atom_supports); + i != port->properties().end() && i->first == uris.atom_supports; + ++i) { + set_property(i->first, i->second); + add.insert(*i); + } + } +} + +void +DuplexPort::on_property(const URI& uri, const Atom& value) +{ + _bufs.engine().driver()->port_property(_path, uri, value); +} + +bool +DuplexPort::get_buffers(BufferFactory& bufs, + PortImpl::GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const +{ + if (!_is_driver_port && is_output()) { + return InputPort::get_buffers(bufs, get, voices, poly, num_in_arcs); + } else if (!_is_driver_port && is_input()) { + return PortImpl::get_buffers(bufs, get, voices, poly, num_in_arcs); + } + return false; +} + +bool +DuplexPort::setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly) +{ + if (!_is_driver_port && is_output()) { + return InputPort::setup_buffers(ctx, bufs, poly); + } else if (!_is_driver_port && is_input()) { + return PortImpl::setup_buffers(ctx, bufs, poly); + } + return false; +} + +void +DuplexPort::set_is_driver_port(BufferFactory& bufs) +{ + _voices->at(0).buffer = new Buffer(bufs, buffer_type(), _value.type(), 0, true, nullptr); + PortImpl::set_is_driver_port(bufs); +} + +void +DuplexPort::set_driver_buffer(void* buf, uint32_t capacity) +{ + _voices->at(0).buffer->set_buffer(buf); + _voices->at(0).buffer->set_capacity(capacity); +} + +uint32_t +DuplexPort::max_tail_poly(RunContext& context) const +{ + return std::max(_poly, parent_graph()->internal_poly_process()); +} + +bool +DuplexPort::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + if (!parent()->parent() || + poly != parent()->parent_graph()->internal_poly()) { + return false; + } + + return PortImpl::prepare_poly(bufs, poly); +} + +bool +DuplexPort::apply_poly(RunContext& context, uint32_t poly) +{ + if (!parent()->parent() || + poly != parent()->parent_graph()->internal_poly()) { + return false; + } + + return PortImpl::apply_poly(context, poly); +} + +void +DuplexPort::pre_process(RunContext& context) +{ + if (_is_output) { + /* This is a graph output, which is an input from the internal + perspective. Prepare buffers for write so plugins can deliver to + them */ + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer->prepare_write(context); + } + } else { + /* This is a a graph input, which is an output from the internal + perspective. Do whatever a normal block's input port does to + prepare input for reading. */ + InputPort::pre_process(context); + InputPort::pre_run(context); + } +} + +void +DuplexPort::post_process(RunContext& context) +{ + if (_is_output) { + /* This is a graph output, which is an input from the internal + perspective. Mix down input delivered by plugins so output + (external perspective) is ready. */ + InputPort::pre_process(context); + InputPort::pre_run(context); + } + monitor(context); +} + +SampleCount +DuplexPort::next_value_offset(SampleCount offset, SampleCount end) const +{ + return PortImpl::next_value_offset(offset, end); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/DuplexPort.hpp b/src/server/DuplexPort.hpp new file mode 100644 index 00000000..b0066164 --- /dev/null +++ b/src/server/DuplexPort.hpp @@ -0,0 +1,98 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_DUPLEXPORT_HPP +#define INGEN_ENGINE_DUPLEXPORT_HPP + +#include <boost/intrusive/slist.hpp> + +#include "BufferRef.hpp" +#include "InputPort.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; + +/** A duplex Port (both an input and output port on a Graph) + * + * This is used for Graph ports, since they need to appear as both an input and + * an output port based on context. There are no actual duplex ports in Ingen, + * a Port is either an Input or Output. This class only exists to allow Graph + * outputs to appear as inputs from within that Graph, and vice versa. + * + * \ingroup engine + */ +class DuplexPort : public InputPort + , public boost::intrusive::slist_base_hook<> // In GraphImpl +{ +public: + DuplexPort(BufferFactory& bufs, + GraphImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + bool polyphonic, + PortType type, + LV2_URID buf_type, + size_t buf_size, + const Atom& value, + bool is_output); + + virtual ~DuplexPort(); + + DuplexPort* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent); + + void inherit_neighbour(const PortImpl* port, + Properties& remove, + Properties& add); + + void on_property(const URI& uri, const Atom& value); + + uint32_t max_tail_poly(RunContext& context) const; + + bool prepare_poly(BufferFactory& bufs, uint32_t poly); + + bool apply_poly(RunContext& context, uint32_t poly); + + bool get_buffers(BufferFactory& bufs, + PortImpl::GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const; + + virtual void set_is_driver_port(BufferFactory& bufs); + + /** Set the external driver-provided buffer. + * + * This may only be called in the process thread, after an earlier call to + * prepare_driver_buffer(). + */ + void set_driver_buffer(void* buf, uint32_t capacity); + + bool setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly); + + void pre_process(RunContext& context); + void post_process(RunContext& context); + + SampleCount next_value_offset(SampleCount offset, SampleCount end) const; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_DUPLEXPORT_HPP diff --git a/src/server/Engine.cpp b/src/server/Engine.cpp new file mode 100644 index 00000000..a7476845 --- /dev/null +++ b/src/server/Engine.cpp @@ -0,0 +1,526 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen_config.h" + +#include <sys/mman.h> + +#include <limits> +#include <thread> + +#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" + +#include "events/CreateGraph.hpp" +#include "ingen/AtomReader.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/Store.hpp" +#include "ingen/StreamWriter.hpp" +#include "ingen/Tee.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "ingen/types.hpp" +#include "raul/Maid.hpp" + +#include "BlockFactory.hpp" +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "ControlBindings.hpp" +#include "DirectDriver.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "Event.hpp" +#include "EventWriter.hpp" +#include "GraphImpl.hpp" +#include "LV2Options.hpp" +#include "PostProcessor.hpp" +#include "PreProcessContext.hpp" +#include "PreProcessor.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" +#include "UndoStack.hpp" +#include "Worker.hpp" +#ifdef HAVE_SOCKET +#include "SocketListener.hpp" +#endif + +namespace Ingen { +namespace Server { + +INGEN_THREAD_LOCAL unsigned ThreadManager::flags(0); +bool ThreadManager::single_threaded(true); + +Engine::Engine(Ingen::World* world) + : _world(world) + , _options(new LV2Options(world->uris())) + , _buffer_factory(new BufferFactory(*this, world->uris())) + , _maid(new Raul::Maid) + , _worker(new Worker(world->log(), event_queue_size())) + , _sync_worker(new Worker(world->log(), event_queue_size(), true)) + , _broadcaster(new Broadcaster()) + , _control_bindings(new ControlBindings(*this)) + , _block_factory(new BlockFactory(world)) + , _undo_stack(new UndoStack(_world->uris(), _world->uri_map())) + , _redo_stack(new UndoStack(_world->uris(), _world->uri_map())) + , _post_processor(new PostProcessor(*this)) + , _pre_processor(new PreProcessor(*this)) + , _event_writer(new EventWriter(*this)) + , _interface(_event_writer) + , _atom_interface( + new AtomReader(world->uri_map(), world->uris(), world->log(), *_interface)) + , _root_graph(nullptr) + , _cycle_start_time(0) + , _rand_engine(0) + , _uniform_dist(0.0f, 1.0f) + , _quit_flag(false) + , _reset_load_flag(false) + , _atomic_bundles(world->conf().option("atomic-bundles").get<int32_t>()) + , _activated(false) +{ + if (!world->store()) { + world->set_store(SPtr<Ingen::Store>(new Store())); + } + + for (int i = 0; i < world->conf().option("threads").get<int32_t>(); ++i) { + Raul::RingBuffer* ring = new Raul::RingBuffer(24 * event_queue_size()); + _notifications.push_back(ring); + _run_contexts.push_back(new RunContext(*this, ring, i, i > 0)); + } + + _world->lv2_features().add_feature(_worker->schedule_feature()); + _world->lv2_features().add_feature(_options); + _world->lv2_features().add_feature( + SPtr<LV2Features::Feature>( + new LV2Features::EmptyFeature(LV2_BUF_SIZE__powerOf2BlockLength))); + _world->lv2_features().add_feature( + SPtr<LV2Features::Feature>( + new LV2Features::EmptyFeature(LV2_BUF_SIZE__fixedBlockLength))); + _world->lv2_features().add_feature( + SPtr<LV2Features::Feature>( + new LV2Features::EmptyFeature(LV2_BUF_SIZE__boundedBlockLength))); + _world->lv2_features().add_feature( + SPtr<LV2Features::Feature>( + new LV2Features::EmptyFeature(LV2_STATE__loadDefaultState))); + + if (world->conf().option("dump").get<int32_t>()) { + _interface = std::make_shared<Tee>( + Tee::Sinks{ + _event_writer, + std::make_shared<StreamWriter>(world->uri_map(), + world->uris(), + URI("ingen:/engine"), + stderr, + ColorContext::Color::MAGENTA)}); + } +} + +Engine::~Engine() +{ + _root_graph = nullptr; + deactivate(); + + // Process all pending events + const FrameTime end = std::numeric_limits<FrameTime>::max(); + RunContext& ctx = run_context(); + locate(ctx.end(), end - ctx.end()); + _post_processor->set_end_time(end); + _post_processor->process(); + while (!_pre_processor->empty()) { + _pre_processor->process(ctx, *_post_processor, 1); + _post_processor->process(); + } + + _atom_interface.reset(); + + // Delete run contexts + _quit_flag = true; + _tasks_available.notify_all(); + for (RunContext* ctx : _run_contexts) { + ctx->join(); + delete ctx; + } + for (Raul::RingBuffer* ring : _notifications) { + delete ring; + } + + const SPtr<Store> store = this->store(); + if (store) { + for (auto& s : *store.get()) { + if (!dynamic_ptr_cast<NodeImpl>(s.second)->parent()) { + s.second.reset(); + } + } + store->clear(); + } + + _world->set_store(SPtr<Ingen::Store>()); +} + +void +Engine::listen() +{ +#ifdef HAVE_SOCKET + _listener = UPtr<SocketListener>(new SocketListener(*this)); +#endif +} + +void +Engine::advance(SampleCount nframes) +{ + for (RunContext* ctx : _run_contexts) { + ctx->locate(ctx->start() + nframes, block_length()); + } +} + +void +Engine::locate(FrameTime s, SampleCount nframes) +{ + for (RunContext* ctx : _run_contexts) { + ctx->locate(s, nframes); + } +} + +void +Engine::set_root_graph(GraphImpl* graph) +{ + _root_graph = graph; +} + +void +Engine::flush_events(const std::chrono::milliseconds& sleep_ms) +{ + bool finished = !pending_events(); + while (!finished) { + // Run one audio block to execute prepared events + run(block_length()); + advance(block_length()); + + // Run one main iteration to post-process events + main_iteration(); + + // Sleep before continuing if there are still events to process + if (!(finished = !pending_events())) { + std::this_thread::sleep_for(sleep_ms); + } + } +} + +void +Engine::emit_notifications(FrameTime end) +{ + for (RunContext* ctx : _run_contexts) { + ctx->emit_notifications(end); + } +} + +bool +Engine::pending_notifications() +{ + for (const RunContext* ctx : _run_contexts) { + if (ctx->pending_notifications()) { + return true; + } + } + return false; +} + +bool +Engine::wait_for_tasks() +{ + if (!_quit_flag) { + std::unique_lock<std::mutex> lock(_tasks_mutex); + _tasks_available.wait(lock); + } + return !_quit_flag; +} + +void +Engine::signal_tasks_available() +{ + _tasks_available.notify_all(); +} + +Task* +Engine::steal_task(unsigned start_thread) +{ + for (unsigned i = 0; i < _run_contexts.size(); ++i) { + const unsigned id = (start_thread + i) % _run_contexts.size(); + RunContext* const ctx = _run_contexts[id]; + Task* par = ctx->task(); + if (par) { + Task* t = par->steal(*ctx); + if (t) { + return t; + } + } + } + return nullptr; +} + +SPtr<Store> +Engine::store() const +{ + return _world->store(); +} + +SampleRate +Engine::sample_rate() const +{ + return _driver->sample_rate(); +} + +SampleCount +Engine::block_length() const +{ + return _driver->block_length(); +} + +size_t +Engine::sequence_size() const +{ + return _driver->seq_size(); +} + +size_t +Engine::event_queue_size() const +{ + return world()->conf().option("queue-size").get<int32_t>(); +} + +void +Engine::quit() +{ + _quit_flag = true; +} + +Properties +Engine::load_properties() const +{ + const Ingen::URIs& uris = world()->uris(); + + return { { uris.ingen_meanRunLoad, + uris.forge.make(floorf(_run_load.mean) / 100.0f) }, + { uris.ingen_minRunLoad, + uris.forge.make(_run_load.min / 100.0f) }, + { uris.ingen_maxRunLoad, + uris.forge.make(_run_load.max / 100.0f) } }; +} + +bool +Engine::main_iteration() +{ + _post_processor->process(); + _maid->cleanup(); + + if (_run_load.changed) { + _broadcaster->put(URI("ingen:/engine"), load_properties()); + _run_load.changed = false; + } + + return !_quit_flag; +} + +void +Engine::set_driver(SPtr<Driver> driver) +{ + _driver = driver; + for (RunContext* ctx : _run_contexts) { + ctx->set_priority(driver->real_time_priority()); + ctx->set_rate(driver->sample_rate()); + } + + _buffer_factory->set_block_length(driver->block_length()); + _options->set(sample_rate(), + block_length(), + buffer_factory()->default_size(_world->uris().atom_Sequence)); +} + +SampleCount +Engine::event_time() +{ + if (ThreadManager::single_threaded) { + return 0; + } + + return _driver->frame_time() + _driver->block_length(); +} + +uint64_t +Engine::current_time() const +{ + return _clock.now_microseconds(); +} + +void +Engine::reset_load() +{ + _reset_load_flag = true; +} + +void +Engine::init(double sample_rate, uint32_t block_length, size_t seq_size) +{ + set_driver(SPtr<Driver>(new DirectDriver(*this, sample_rate, block_length, seq_size))); +} + +bool +Engine::supports_dynamic_ports() const +{ + return !_driver || _driver->dynamic_ports(); +} + +bool +Engine::activate() +{ + if (!_driver) { + return false; + } + + ThreadManager::single_threaded = true; + + const Ingen::URIs& uris = world()->uris(); + + if (!_root_graph) { + // No root graph has been loaded, create an empty one + const Properties properties = { + {uris.rdf_type, uris.ingen_Graph}, + {uris.ingen_polyphony, + Property(_world->forge().make(1), + Resource::Graph::INTERNAL)}}; + + enqueue_event( + new Events::CreateGraph( + *this, SPtr<Interface>(), -1, 0, Raul::Path("/"), properties)); + + flush_events(std::chrono::milliseconds(10)); + if (!_root_graph) { + return false; + } + } + + _driver->activate(); + _root_graph->enable(); + + ThreadManager::single_threaded = false; + _activated = true; + + return true; +} + +void +Engine::deactivate() +{ + if (_driver) { + _driver->deactivate(); + } + + if (_root_graph) { + _root_graph->deactivate(); + } + + ThreadManager::single_threaded = true; + _activated = false; +} + +unsigned +Engine::run(uint32_t sample_count) +{ + RunContext& ctx = run_context(); + _cycle_start_time = current_time(); + + post_processor()->set_end_time(ctx.end()); + + // Process events that came in during the last cycle + // (Aiming for jitter-free 1 block event latency, ideally) + const unsigned n_processed_events = process_events(); + + // Reset load if graph structure has changed + if (_reset_load_flag) { + _run_load = Load(); + _reset_load_flag = false; + } + + // Run root graph + if (_root_graph) { + // Apply control bindings to input + control_bindings()->pre_process( + ctx, _root_graph->port_impl(0)->buffer(0).get()); + + // Run root graph for this cycle + _root_graph->process(ctx); + + // Emit control binding feedback + control_bindings()->post_process( + ctx, _root_graph->port_impl(1)->buffer(0).get()); + } + + // Update load for this cycle + if (ctx.duration() > 0) { + _run_load.update(current_time() - _cycle_start_time, ctx.duration()); + } + + return n_processed_events; +} + +bool +Engine::pending_events() const +{ + return !_pre_processor->empty() || _post_processor->pending(); +} + +void +Engine::enqueue_event(Event* ev, Event::Mode mode) +{ + _pre_processor->event(ev, mode); +} + +unsigned +Engine::process_events() +{ + const size_t MAX_EVENTS_PER_CYCLE = run_context().nframes() / 8; + return _pre_processor->process( + run_context(), *_post_processor, MAX_EVENTS_PER_CYCLE); +} + +unsigned +Engine::process_all_events() +{ + return _pre_processor->process(run_context(), *_post_processor, 0); +} + +Log& +Engine::log() const +{ + return _world->log(); +} + +void +Engine::register_client(SPtr<Interface> client) +{ + log().info(fmt("Registering client <%1%>\n") % client->uri().c_str()); + _broadcaster->register_client(client); +} + +bool +Engine::unregister_client(SPtr<Interface> client) +{ + log().info(fmt("Unregistering client <%1%>\n") % client->uri().c_str()); + return _broadcaster->unregister_client(client); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Engine.hpp b/src/server/Engine.hpp new file mode 100644 index 00000000..f5ba1feb --- /dev/null +++ b/src/server/Engine.hpp @@ -0,0 +1,221 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_ENGINE_HPP +#define INGEN_ENGINE_ENGINE_HPP + +#include <chrono> +#include <condition_variable> +#include <mutex> +#include <random> + +#include "ingen/Clock.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Properties.hpp" +#include "ingen/ingen.h" +#include "ingen/types.hpp" + +#include "Event.hpp" +#include "Load.hpp" + +namespace Raul { +class Maid; +class RingBuffer; +} + +namespace Ingen { + +class AtomReader; +class Interface; +class Log; +class Store; +class World; + +namespace Server { + +class BlockFactory; +class Broadcaster; +class BufferFactory; +class ControlBindings; +class Driver; +class EventWriter; +class GraphImpl; +class LV2Options; +class PostProcessor; +class PreProcessor; +class RunContext; +class SocketListener; +class Task; +class UndoStack; +class Worker; + +/** + The engine which executes the process graph. + + This is a simple class that provides pointers to the various components + that make up the engine implementation. In processes with a local engine, + it can be accessed via the Ingen::World. + + @ingroup engine +*/ +class INGEN_API Engine : public EngineBase +{ +public: + explicit Engine(Ingen::World* world); + virtual ~Engine(); + + Engine(const Engine&) = delete; + Engine& operator=(const Engine&) = delete; + + // EngineBase methods + virtual void init(double sample_rate, uint32_t block_length, size_t seq_size); + virtual bool supports_dynamic_ports() const; + virtual bool activate(); + virtual void deactivate(); + virtual bool pending_events() const; + virtual unsigned run(uint32_t sample_count); + virtual void quit(); + virtual bool main_iteration(); + virtual void register_client(SPtr<Interface> client); + virtual bool unregister_client(SPtr<Interface> client); + + void listen(); + + /** Return a random [0..1] float with uniform distribution */ + float frand() { return _uniform_dist(_rand_engine); } + + void set_driver(SPtr<Driver> driver); + + /** Return the frame time to execute an event that arrived now. + * + * This aims to return a time one cycle from "now", so that events ideally + * have 1 cycle of latency with no jitter. + */ + SampleCount event_time(); + + /** Return the time this cycle began processing in microseconds. + * + * This value is comparable to the value returned by current_time(). + */ + inline uint64_t cycle_start_time(const RunContext& context) const { + return _cycle_start_time; + } + + /** Return the current time in microseconds. */ + uint64_t current_time() const; + + /** Reset the load statistics (when the expected DSP load changes). */ + void reset_load(); + + /** Enqueue an event to be processed (non-realtime threads only). */ + void enqueue_event(Event* ev, Event::Mode mode=Event::Mode::NORMAL); + + /** Process events (process thread only). */ + unsigned process_events(); + + /** Process all events (no RT limits). */ + unsigned process_all_events(); + + Ingen::World* world() const { return _world; } + Log& log() const; + + const SPtr<Interface>& interface() const { return _interface; } + const SPtr<EventWriter>& event_writer() const { return _event_writer; } + const UPtr<AtomReader>& atom_interface() const { return _atom_interface; } + const UPtr<BlockFactory>& block_factory() const { return _block_factory; } + const UPtr<Broadcaster>& broadcaster() const { return _broadcaster; } + const UPtr<BufferFactory>& buffer_factory() const { return _buffer_factory; } + const UPtr<ControlBindings>& control_bindings() const { return _control_bindings; } + const SPtr<Driver>& driver() const { return _driver; } + const UPtr<PostProcessor>& post_processor() const { return _post_processor; } + const UPtr<Raul::Maid>& maid() const { return _maid; } + const UPtr<UndoStack>& undo_stack() const { return _undo_stack; } + const UPtr<UndoStack>& redo_stack() const { return _redo_stack; } + const UPtr<Worker>& worker() const { return _worker; } + const UPtr<Worker>& sync_worker() const { return _sync_worker; } + + GraphImpl* root_graph() const { return _root_graph; } + void set_root_graph(GraphImpl* graph); + + RunContext& run_context() { return *_run_contexts[0]; } + + void flush_events(const std::chrono::milliseconds& sleep_ms); + + void advance(SampleCount nframes); + void locate(FrameTime s, SampleCount nframes); + void emit_notifications(FrameTime end); + bool pending_notifications(); + bool wait_for_tasks(); + void signal_tasks_available(); + Task* steal_task(unsigned start_thread); + + SPtr<Store> store() const; + + SampleRate sample_rate() const; + SampleCount block_length() const; + size_t sequence_size() const; + size_t event_queue_size() const; + + size_t n_threads() const { return _run_contexts.size(); } + bool atomic_bundles() const { return _atomic_bundles; } + bool activated() const { return _activated; } + + Properties load_properties() const; + +private: + Ingen::World* _world; + + SPtr<LV2Options> _options; + UPtr<BufferFactory> _buffer_factory; + UPtr<Raul::Maid> _maid; + SPtr<Driver> _driver; + UPtr<Worker> _worker; + UPtr<Worker> _sync_worker; + UPtr<Broadcaster> _broadcaster; + UPtr<ControlBindings> _control_bindings; + UPtr<BlockFactory> _block_factory; + UPtr<UndoStack> _undo_stack; + UPtr<UndoStack> _redo_stack; + UPtr<PostProcessor> _post_processor; + UPtr<PreProcessor> _pre_processor; + UPtr<SocketListener> _listener; + SPtr<EventWriter> _event_writer; + SPtr<Interface> _interface; + UPtr<AtomReader> _atom_interface; + GraphImpl* _root_graph; + + std::vector<Raul::RingBuffer*> _notifications; + std::vector<RunContext*> _run_contexts; + uint64_t _cycle_start_time; + Load _run_load; + Clock _clock; + + std::mt19937 _rand_engine; + std::uniform_real_distribution<float> _uniform_dist; + + std::condition_variable _tasks_available; + std::mutex _tasks_mutex; + + bool _quit_flag; + bool _reset_load_flag; + bool _atomic_bundles; + bool _activated; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_ENGINE_HPP diff --git a/src/server/EnginePort.hpp b/src/server/EnginePort.hpp new file mode 100644 index 00000000..c14f363c --- /dev/null +++ b/src/server/EnginePort.hpp @@ -0,0 +1,66 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_ENGINE_PORT_HPP +#define INGEN_ENGINE_ENGINE_PORT_HPP + +#include "raul/Deletable.hpp" +#include "raul/Noncopyable.hpp" + +#include <boost/intrusive/slist.hpp> + +#include "DuplexPort.hpp" + +namespace Ingen { +namespace Server { + +/** A "system" port (e.g. a Jack port, an external port on Ingen). + * + * @ingroup engine + */ +class EnginePort : public Raul::Noncopyable + , public Raul::Deletable + , public boost::intrusive::slist_base_hook<> +{ +public: + explicit EnginePort(DuplexPort* port) + : _graph_port(port) + , _buffer(nullptr) + , _handle(nullptr) + , _driver_index(0) + {} + + void set_buffer(void* buf) { _buffer = buf; } + void set_handle(void* buf) { _handle = buf; } + void set_driver_index(uint32_t index) { _driver_index = index; } + + void* buffer() const { return _buffer; } + void* handle() const { return _handle; } + uint32_t driver_index() const { return _driver_index; } + DuplexPort* graph_port() const { return _graph_port; } + bool is_input() const { return _graph_port->is_input(); } + +protected: + DuplexPort* _graph_port; + void* _buffer; + void* _handle; + uint32_t _driver_index; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_ENGINE_PORT_HPP diff --git a/src/server/Event.hpp b/src/server/Event.hpp new file mode 100644 index 00000000..d9095def --- /dev/null +++ b/src/server/Event.hpp @@ -0,0 +1,163 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_EVENT_HPP +#define INGEN_ENGINE_EVENT_HPP + +#include <atomic> + +#include "raul/Deletable.hpp" +#include "raul/Noncopyable.hpp" +#include "raul/Path.hpp" + +#include "ingen/Interface.hpp" +#include "ingen/Node.hpp" +#include "ingen/Status.hpp" +#include "ingen/types.hpp" + +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class RunContext; +class PreProcessContext; + +/** An event (command) to perform some action on Ingen. + * + * Virtually all operations on Ingen are implemented as events. An event has + * three distinct execution phases: + * + * 1) Pre-process: In a non-realtime thread, prepare event for execution + * 2) Execute: In the audio thread, execute (apply) event + * 3) Post-process: In a non-realtime thread, finalize event + * (e.g. clean up and send replies) + * + * \ingroup engine + */ +class Event : public Raul::Deletable, public Raul::Noncopyable +{ +public: + /** Event mode to distinguish normal events from undo events. */ + enum class Mode { NORMAL, UNDO, REDO }; + + /** Execution mode for events that block and unblock preprocessing. */ + enum class Execution { + NORMAL, ///< Normal pipelined execution + ATOMIC, ///< Block pre-processing until this event is executed + BLOCK, ///< Begin atomic block of events + UNBLOCK ///< Finish atomic executed block of events + }; + + /** Pre-process event before execution (non-realtime). */ + virtual bool pre_process(PreProcessContext& ctx) = 0; + + /** Execute this event in the audio thread (realtime). */ + virtual void execute(RunContext& context) = 0; + + /** Post-process event after execution (non-realtime). */ + virtual void post_process() = 0; + + /** Write the inverse of this event to `sink`. */ + virtual void undo(Interface& target) {} + + /** Return true iff this event has been pre-processed. */ + inline bool is_prepared() const { return _status != Status::NOT_PREPARED; } + + /** Return the time stamp of this event. */ + inline SampleCount time() const { return _time; } + + /** Set the time stamp of this event. */ + inline void set_time(SampleCount time) { _time = time; } + + /** Get the next event to be processed after this one. */ + Event* next() const { return _next.load(); } + + /** Set the next event to be processed after this one. */ + void next(Event* ev) { _next = ev; } + + /** Return the status (success or error code) of this event. */ + Status status() const { return _status; } + + /** Return the blocking behaviour of this event (after construction). */ + virtual Execution get_execution() const { return Execution::NORMAL; } + + /** Return undo mode of this event. */ + Mode get_mode() const { return _mode; } + + /** Set the undo mode of this event. */ + void set_mode(Mode mode) { _mode = mode; } + + inline Engine& engine() { return _engine; } + +protected: + Event(Engine& engine, SPtr<Interface> client, int32_t id, FrameTime time) + : _engine(engine) + , _next(nullptr) + , _request_client(std::move(client)) + , _request_id(id) + , _time(time) + , _status(Status::NOT_PREPARED) + , _mode(Mode::NORMAL) + {} + + /** Constructor for internal events only */ + explicit Event(Engine& engine) + : _engine(engine) + , _next(nullptr) + , _request_id(0) + , _time(0) + , _status(Status::NOT_PREPARED) + , _mode(Mode::NORMAL) + {} + + inline bool pre_process_done(Status st) { + _status = st; + return st == Status::SUCCESS; + } + + inline bool pre_process_done(Status st, const URI& subject) { + _err_subject = subject; + return pre_process_done(st); + } + + inline bool pre_process_done(Status st, const Raul::Path& subject) { + return pre_process_done(st, path_to_uri(subject)); + } + + /** Respond to the originating client. */ + inline Status respond() { + if (_request_client && _request_id) { + _request_client->response(_request_id, _status, _err_subject); + } + return _status; + } + + Engine& _engine; + std::atomic<Event*> _next; + SPtr<Interface> _request_client; + int32_t _request_id; + FrameTime _time; + Status _status; + std::string _err_subject; + Mode _mode; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_EVENT_HPP diff --git a/src/server/EventWriter.cpp b/src/server/EventWriter.cpp new file mode 100644 index 00000000..ebdf7562 --- /dev/null +++ b/src/server/EventWriter.cpp @@ -0,0 +1,147 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <boost/variant/apply_visitor.hpp> + +#include "ingen/URIs.hpp" + +#include "Engine.hpp" +#include "EventWriter.hpp" +#include "events.hpp" + +namespace Ingen { +namespace Server { + +EventWriter::EventWriter(Engine& engine) + : _engine(engine) + , _event_mode(Event::Mode::NORMAL) +{ +} + +SampleCount +EventWriter::now() const +{ + return _engine.event_time(); +} + +void +EventWriter::message(const Message& msg) +{ + boost::apply_visitor(*this, msg); +} + +void +EventWriter::operator()(const BundleBegin& msg) +{ + _engine.enqueue_event(new Events::Mark(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const BundleEnd& msg) +{ + _engine.enqueue_event(new Events::Mark(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Put& msg) +{ + _engine.enqueue_event(new Events::Delta(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Delta& msg) +{ + _engine.enqueue_event(new Events::Delta(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Copy& msg) +{ + _engine.enqueue_event(new Events::Copy(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Move& msg) +{ + _engine.enqueue_event(new Events::Move(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Del& msg) +{ + _engine.enqueue_event(new Events::Delete(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Connect& msg) +{ + _engine.enqueue_event(new Events::Connect(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Disconnect& msg) +{ + _engine.enqueue_event( + new Events::Disconnect(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const DisconnectAll& msg) +{ + _engine.enqueue_event( + new Events::DisconnectAll(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const SetProperty& msg) +{ + _engine.enqueue_event(new Events::Delta(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Undo& msg) +{ + _engine.enqueue_event(new Events::Undo(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Redo& msg) +{ + _engine.enqueue_event(new Events::Undo(_engine, _respondee, now(), msg), + _event_mode); +} + +void +EventWriter::operator()(const Get& msg) +{ + _engine.enqueue_event(new Events::Get(_engine, _respondee, now(), msg), + _event_mode); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/EventWriter.hpp b/src/server/EventWriter.hpp new file mode 100644 index 00000000..2d4b9724 --- /dev/null +++ b/src/server/EventWriter.hpp @@ -0,0 +1,86 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_EVENTWRITER_HPP +#define INGEN_ENGINE_EVENTWRITER_HPP + +#include <memory> +#include <string> + +#include "ingen/Interface.hpp" +#include "ingen/Resource.hpp" +#include "ingen/types.hpp" + +#include "Event.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class Engine; + +/** An Interface that creates and enqueues Events for the Engine to execute. + */ +class EventWriter : public Interface +{ +public: + explicit EventWriter(Engine& engine); + + URI uri() const override { return URI("ingen:/clients/event_writer"); } + + SPtr<Interface> respondee() const override { + return _respondee; + } + + void set_respondee(SPtr<Interface> respondee) override { + _respondee = respondee; + } + + void message(const Message& msg) override; + + void set_event_mode(Event::Mode mode) { _event_mode = mode; } + Event::Mode get_event_mode() { return _event_mode; } + + void operator()(const BundleBegin&); + void operator()(const BundleEnd&); + void operator()(const Connect&); + void operator()(const Copy&); + void operator()(const Del&); + void operator()(const Delta&); + void operator()(const Disconnect&); + void operator()(const DisconnectAll&); + void operator()(const Error&) {} + void operator()(const Get&); + void operator()(const Move&); + void operator()(const Put&); + void operator()(const Redo&); + void operator()(const Response&) {} + void operator()(const SetProperty&); + void operator()(const Undo&); + +protected: + Engine& _engine; + SPtr<Interface> _respondee; + Event::Mode _event_mode; + +private: + SampleCount now() const; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_EVENTWRITER_HPP diff --git a/src/server/FrameTimer.hpp b/src/server/FrameTimer.hpp new file mode 100644 index 00000000..367ac900 --- /dev/null +++ b/src/server/FrameTimer.hpp @@ -0,0 +1,110 @@ +/* + This file is part of Ingen. + Copyright 2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_FRAMETIMER_HPP +#define INGEN_ENGINE_FRAMETIMER_HPP + +#include <chrono> +#include <cmath> +#include <cstdint> + +namespace Ingen { +namespace Server { + +/** Delay-locked loop for monotonic sample time. + * + * See "Using a DLL to filter time" by Fons Adriaensen + * http://kokkinizita.linuxaudio.org/papers/usingdll.pdf + */ +class FrameTimer +{ +public: + static constexpr double PI = 3.14159265358979323846; + static constexpr double bandwidth = 1.0 / 8.0; // Hz + static constexpr double us_per_s = 1000000.0; + + FrameTimer(uint32_t period_size, uint32_t sample_rate) + : tper(((double)period_size / (double)sample_rate) * us_per_s) + , omega(2 * PI * bandwidth / us_per_s * tper) + , b(sqrt(2) * omega) + , c(omega * omega) + , nper(period_size) + { + } + + /** Update the timer for current real time `usec` and frame `frame`. */ + void update(uint64_t usec, uint64_t frame) { + if (!initialized || frame != n1) { + init(usec, frame); + return; + } + + // Calculate loop error + const double e = ((double)usec - t1); + + // Update loop + t0 = t1; + t1 += b * e + e2; + e2 += c * e; + + // Update frame counts + n0 = n1; + n1 += nper; + } + + /** Return an estimate of the frame time for current real time `usec`. */ + uint64_t frame_time(uint64_t usec) const { + if (!initialized) { + return 0; + } + + const double delta = (double)usec - t0; + const double period = t1 - t0; + return n0 + std::round(delta / period * nper); + } + +private: + void init(uint64_t now, uint64_t frame) { + // Init loop + e2 = tper; + t0 = now; + t1 = t0 + e2; + + // Init sample counts + n0 = frame; + n1 = n0 + nper; + + initialized = true; + } + + const double tper; + const double omega; + const double b; + const double c; + + uint64_t nper; + double e2; + double t0; + double t1; + uint64_t n0; + uint64_t n1; + bool initialized; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_FRAMETIMER_HPP diff --git a/src/server/GraphImpl.cpp b/src/server/GraphImpl.cpp new file mode 100644 index 00000000..f9c4cb54 --- /dev/null +++ b/src/server/GraphImpl.cpp @@ -0,0 +1,379 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <unordered_map> + +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "raul/Maid.hpp" + +#include "ArcImpl.hpp" +#include "BlockImpl.hpp" +#include "BufferFactory.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "GraphPlugin.hpp" +#include "PortImpl.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +GraphImpl::GraphImpl(Engine& engine, + const Raul::Symbol& symbol, + uint32_t poly, + GraphImpl* parent, + SampleRate srate, + uint32_t internal_poly) + : BlockImpl(new GraphPlugin(engine.world()->uris(), + engine.world()->uris().ingen_Graph, + Raul::Symbol("graph"), + "Ingen Graph"), + symbol, poly, parent, srate) + , _engine(engine) + , _poly_pre(internal_poly) + , _poly_process(internal_poly) + , _process(false) +{ + assert(internal_poly >= 1); + assert(internal_poly <= 128); +} + +GraphImpl::~GraphImpl() +{ + delete _plugin; +} + +BlockImpl* +GraphImpl::duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) +{ + BufferFactory& bufs = *engine.buffer_factory(); + const SampleRate rate = engine.sample_rate(); + + // Duplicate graph + GraphImpl* dup = new GraphImpl( + engine, symbol, _polyphony, parent, rate, _poly_process); + + Properties props = properties(); + props.erase(bufs.uris().lv2_symbol); + props.insert({bufs.uris().lv2_symbol, bufs.forge().alloc(symbol.c_str())}); + dup->set_properties(props); + + // We need a map of port duplicates to duplicate arcs + typedef std::unordered_map<PortImpl*, PortImpl*> PortMap; + PortMap port_map; + + // Add duplicates of all ports + dup->_ports = bufs.maid().make_managed<Ports>(num_ports(), nullptr); + for (PortList::iterator p = _inputs.begin(); p != _inputs.end(); ++p) { + DuplexPort* p_dup = p->duplicate(engine, p->symbol(), dup); + dup->_inputs.push_front(*p_dup); + (*dup->_ports)[p->index()] = p_dup; + port_map.insert({&*p, p_dup}); + } + for (PortList::iterator p = _outputs.begin(); p != _outputs.end(); ++p) { + DuplexPort* p_dup = p->duplicate(engine, p->symbol(), dup); + dup->_outputs.push_front(*p_dup); + (*dup->_ports)[p->index()] = p_dup; + port_map.insert({&*p, p_dup}); + } + + // Add duplicates of all blocks + for (auto& b : _blocks) { + BlockImpl* b_dup = b.duplicate(engine, b.symbol(), dup); + dup->add_block(*b_dup); + b_dup->activate(*engine.buffer_factory()); + for (uint32_t p = 0; p < b.num_ports(); ++p) { + port_map.insert({b.port_impl(p), b_dup->port_impl(p)}); + } + } + + // Add duplicates of all arcs + for (const auto& a : _arcs) { + SPtr<ArcImpl> arc = dynamic_ptr_cast<ArcImpl>(a.second); + if (arc) { + auto t = port_map.find(arc->tail()); + auto h = port_map.find(arc->head()); + if (t != port_map.end() && h != port_map.end()) { + dup->add_arc(SPtr<ArcImpl>(new ArcImpl(t->second, h->second))); + } + } + } + + return dup; +} + +void +GraphImpl::activate(BufferFactory& bufs) +{ + BlockImpl::activate(bufs); + + for (auto& b : _blocks) { + b.activate(bufs); + } + + assert(_activated); +} + +void +GraphImpl::deactivate() +{ + if (_activated) { + BlockImpl::deactivate(); + + for (auto& b : _blocks) { + if (b.activated()) { + b.deactivate(); + } + } + } +} + +void +GraphImpl::disable(RunContext& context) +{ + _process = false; + for (auto& o : _outputs) { + o.clear_buffers(context); + } +} + +bool +GraphImpl::prepare_internal_poly(BufferFactory& bufs, uint32_t poly) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + // TODO: Subgraph dynamic polyphony (i.e. changing port polyphony) + + for (auto& b : _blocks) { + b.prepare_poly(bufs, poly); + } + + _poly_pre = poly; + return true; +} + +bool +GraphImpl::apply_internal_poly(RunContext& context, + BufferFactory& bufs, + Raul::Maid& maid, + uint32_t poly) +{ + // TODO: Subgraph dynamic polyphony (i.e. changing port polyphony) + + for (auto& b : _blocks) { + b.apply_poly(context, poly); + } + + for (auto& b : _blocks) { + for (uint32_t j = 0; j < b.num_ports(); ++j) { + PortImpl* const port = b.port_impl(j); + if (port->is_input() && dynamic_cast<InputPort*>(port)->direct_connect()) { + port->setup_buffers(context, bufs, port->poly()); + } + port->connect_buffers(); + } + } + + const bool polyphonic = parent_graph() && (poly == parent_graph()->internal_poly_process()); + for (auto& o : _outputs) { + o.setup_buffers(context, bufs, polyphonic ? poly : 1); + } + + _poly_process = poly; + return true; +} + +void +GraphImpl::pre_process(RunContext& context) +{ + // Mix down input ports and connect buffers + for (uint32_t i = 0; i < num_ports(); ++i) { + PortImpl* const port = _ports->at(i); + if (!port->is_driver_port()) { + port->pre_process(context); + port->pre_run(context); + port->connect_buffers(); + } + } +} + +void +GraphImpl::process(RunContext& context) +{ + if (!_process) { + return; + } + + pre_process(context); + run(context); + post_process(context); +} + +void +GraphImpl::run(RunContext& context) +{ + if (_compiled_graph) { + _compiled_graph->run(context); + } +} + +void +GraphImpl::set_buffer_size(RunContext& context, + BufferFactory& bufs, + LV2_URID type, + uint32_t size) +{ + BlockImpl::set_buffer_size(context, bufs, type, size); + + if (_compiled_graph) { + // FIXME + // for (size_t i = 0; i < _compiled_graph->size(); ++i) { + // const CompiledBlock& block = (*_compiled_graph)[i]; + // block.block()->set_buffer_size(context, bufs, type, size); + // } + } +} + +void +GraphImpl::add_block(BlockImpl& block) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + _blocks.push_front(block); +} + +void +GraphImpl::remove_block(BlockImpl& block) +{ + _blocks.erase(_blocks.iterator_to(block)); +} + +void +GraphImpl::add_arc(SPtr<ArcImpl> a) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + _arcs.emplace(std::make_pair(a->tail(), a->head()), a); +} + +SPtr<ArcImpl> +GraphImpl::remove_arc(const PortImpl* tail, const PortImpl* dst_port) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + auto i = _arcs.find(std::make_pair(tail, dst_port)); + if (i != _arcs.end()) { + SPtr<ArcImpl> arc = dynamic_ptr_cast<ArcImpl>(i->second); + _arcs.erase(i); + return arc; + } else { + return SPtr<ArcImpl>(); + } +} + +bool +GraphImpl::has_arc(const PortImpl* tail, const PortImpl* dst_port) const +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + auto i = _arcs.find(std::make_pair(tail, dst_port)); + return (i != _arcs.end()); +} + +void +GraphImpl::set_compiled_graph(MPtr<CompiledGraph>&& cg) +{ + if (_compiled_graph && _compiled_graph != cg) { + _engine.reset_load(); + } + _compiled_graph = std::move(cg); +} + +uint32_t +GraphImpl::num_ports_non_rt() const +{ + ThreadManager::assert_not_thread(THREAD_PROCESS); + return _inputs.size() + _outputs.size(); +} + +bool +GraphImpl::has_port_with_index(uint32_t index) const +{ + BufferFactory& bufs = *_engine.buffer_factory(); + const auto index_atom = bufs.forge().make(int32_t(index)); + + for (auto p = _inputs.begin(); p != _inputs.end(); ++p) { + if (p->has_property(bufs.uris().lv2_index, index_atom)) { + return true; + } + } + + for (auto p = _outputs.begin(); p != _outputs.end(); ++p) { + if (p->has_property(bufs.uris().lv2_index, index_atom)) { + return true; + } + } + + return false; +} + +void +GraphImpl::remove_port(DuplexPort& port) +{ + if (port.is_input()) { + _inputs.erase(_inputs.iterator_to(port)); + } else { + _outputs.erase(_outputs.iterator_to(port)); + } +} + +void +GraphImpl::clear_ports() +{ + _inputs.clear(); + _outputs.clear(); +} + +MPtr<BlockImpl::Ports> +GraphImpl::build_ports_array(Raul::Maid& maid) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + const size_t n = _inputs.size() + _outputs.size(); + MPtr<Ports> result = maid.make_managed<Ports>(n); + + std::map<size_t, DuplexPort*> ports; + for (PortList::iterator p = _inputs.begin(); p != _inputs.end(); ++p) { + ports.emplace(p->index(), &*p); + } + for (PortList::iterator p = _outputs.begin(); p != _outputs.end(); ++p) { + ports.emplace(p->index(), &*p); + } + + size_t i = 0; + for (const auto& p : ports) { + result->at(i++) = p.second; + } + + assert(i == n); + + return result; +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/GraphImpl.hpp b/src/server/GraphImpl.hpp new file mode 100644 index 00000000..3f11a84a --- /dev/null +++ b/src/server/GraphImpl.hpp @@ -0,0 +1,200 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_GRAPHIMPL_HPP +#define INGEN_ENGINE_GRAPHIMPL_HPP + +#include <cstdlib> + +#include "ingen/ingen.h" + +#include "BlockImpl.hpp" +#include "CompiledGraph.hpp" +#include "DuplexPort.hpp" +#include "PluginImpl.hpp" +#include "PortType.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { + +class Arc; + +namespace Server { + +class ArcImpl; +class CompiledGraph; +class Engine; +class RunContext; + +/** A group of blocks in a graph, possibly polyphonic. + * + * Note that this is also a Block, just one which contains Blocks. + * Therefore infinite subgraphing is possible, of polyphonic + * graphs of polyphonic blocks etc. etc. + * + * \ingroup engine + */ +class GraphImpl : public BlockImpl +{ +public: + GraphImpl(Engine& engine, + const Raul::Symbol& symbol, + uint32_t poly, + GraphImpl* parent, + SampleRate srate, + uint32_t internal_poly); + + virtual ~GraphImpl(); + + virtual GraphType graph_type() const { return GraphType::GRAPH; } + + BlockImpl* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent); + + void activate(BufferFactory& bufs); + void deactivate(); + + void pre_process(RunContext& context); + void process(RunContext& context); + void run(RunContext& context); + + void set_buffer_size(RunContext& context, + BufferFactory& bufs, + LV2_URID type, + uint32_t size); + + /** Prepare for a new (internal) polyphony value. + * + * Pre-process thread, poly is actually applied by apply_internal_poly. + * \return true on success. + */ + bool prepare_internal_poly(BufferFactory& bufs, uint32_t poly); + + /** Apply a new (internal) polyphony value. + * + * Audio thread. + * + * \param context Process context + * \param bufs New set of buffers + * \param poly Must be < the most recent value passed to prepare_internal_poly. + * \param maid Any objects no longer needed will be pushed to this + */ + bool apply_internal_poly(RunContext& context, + BufferFactory& bufs, + Raul::Maid& maid, + uint32_t poly); + + // Graph specific stuff not inherited from Block + + typedef boost::intrusive::slist< + BlockImpl, boost::intrusive::constant_time_size<true> > Blocks; + + /** Add a block to this graph. + * Pre-process thread only. + */ + void add_block(BlockImpl& block); + + /** Remove a block from this graph. + * Pre-process thread only. + */ + void remove_block(BlockImpl& block); + + Blocks& blocks() { return _blocks; } + const Blocks& blocks() const { return _blocks; } + + uint32_t num_ports_non_rt() const; + bool has_port_with_index(uint32_t index) const; + + typedef boost::intrusive::slist< + DuplexPort, boost::intrusive::constant_time_size<true> > PortList; + + void add_input(DuplexPort& port) { + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + assert(port.is_input()); + _inputs.push_front(port); + } + + void add_output(DuplexPort& port) { + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + assert(port.is_output()); + _outputs.push_front(port); + } + + /** Remove port from ports list used in pre-processing thread. + * + * Port is not removed from ports array for process thread (which could be + * simultaneously running). + * + * Pre-processing thread or situations that won't cause races with it only. + */ + void remove_port(DuplexPort& port); + + /** Remove all ports from ports list used in pre-processing thread. + * + * Ports are not removed from ports array for process thread (which could be + * simultaneously running). Returned is a (inputs, outputs) pair. + * + * Pre-processing thread or situations that won't cause races with it only. + */ + void clear_ports(); + + /** Add an arc to this graph. + * Pre-processing thread only. + */ + void add_arc(SPtr<ArcImpl> a); + + /** Remove an arc from this graph. + * Pre-processing thread only. + */ + SPtr<ArcImpl> remove_arc(const PortImpl* tail, const PortImpl* dst_port); + + bool has_arc(const PortImpl* tail, const PortImpl* dst_port) const; + + /** Set a new compiled graph to run, and return the old one. */ + void set_compiled_graph(MPtr<CompiledGraph>&& cg); + + const MPtr<Ports>& external_ports() { return _ports; } + + void set_external_ports(MPtr<Ports>&& pa) { _ports = std::move(pa); } + + MPtr<Ports> build_ports_array(Raul::Maid& maid); + + /** Whether to run this graph's DSP bits in the audio thread */ + bool enabled() const { return _process; } + void enable() { _process = true; } + void disable(RunContext& context); + + uint32_t internal_poly() const { return _poly_pre; } + uint32_t internal_poly_process() const { return _poly_process; } + + Engine& engine() { return _engine; } + +private: + Engine& _engine; + uint32_t _poly_pre; ///< Pre-process thread only + uint32_t _poly_process; ///< Process thread only + MPtr<CompiledGraph> _compiled_graph; ///< Process thread only + PortList _inputs; ///< Pre-process thread only + PortList _outputs; ///< Pre-process thread only + Blocks _blocks; ///< Pre-process thread only + bool _process; ///< True iff graph is enabled +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_GRAPHIMPL_HPP diff --git a/src/server/GraphPlugin.hpp b/src/server/GraphPlugin.hpp new file mode 100644 index 00000000..308ed91a --- /dev/null +++ b/src/server/GraphPlugin.hpp @@ -0,0 +1,63 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_GRAPHPLUGIN_HPP +#define INGEN_ENGINE_GRAPHPLUGIN_HPP + +#include <string> +#include "PluginImpl.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; + +/** Implementation of a Graph plugin. + * + * Graphs don't actually work like this yet... + */ +class GraphPlugin : public PluginImpl +{ +public: + GraphPlugin(URIs& uris, + const URI& uri, + const Raul::Symbol& symbol, + const std::string& name) + : PluginImpl(uris, uris.ingen_Graph.urid, uri) + {} + + BlockImpl* instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state) + { + return nullptr; + } + + const Raul::Symbol symbol() const { return Raul::Symbol("graph"); } + const std::string name() const { return "Ingen Graph"; } + +private: + const std::string _symbol; + const std::string _name; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_GRAPHPLUGIN_HPP diff --git a/src/server/InputPort.cpp b/src/server/InputPort.cpp new file mode 100644 index 00000000..2f22491f --- /dev/null +++ b/src/server/InputPort.cpp @@ -0,0 +1,261 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <cassert> + +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" + +#include "ArcImpl.hpp" +#include "BlockImpl.hpp" +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "RunContext.hpp" +#include "mix.hpp" + +namespace Ingen { +namespace Server { + +InputPort::InputPort(BufferFactory& bufs, + BlockImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size) + : PortImpl(bufs, parent, symbol, index, poly, type, buffer_type, value, buffer_size, false) + , _num_arcs(0) +{ + const Ingen::URIs& uris = bufs.uris(); + + if (parent->graph_type() != Node::GraphType::GRAPH) { + add_property(uris.rdf_type, uris.lv2_InputPort.urid); + } +} + +bool +InputPort::apply_poly(RunContext& context, uint32_t poly) +{ + bool ret = PortImpl::apply_poly(context, poly); + if (!ret) { + poly = 1; + } + + assert(_voices->size() >= poly); + + return true; +} + +bool +InputPort::get_buffers(BufferFactory& bufs, + PortImpl::GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const +{ + if (is_a(PortType::ATOM) && !_value.is_valid()) { + poly = 1; + } + + if (is_a(PortType::AUDIO) && num_in_arcs == 0) { + // Audio input with no arcs, use shared zero buffer + for (uint32_t v = 0; v < poly; ++v) { + voices->at(v).buffer = bufs.silent_buffer(); + } + return false; + } + + // Otherwise, allocate local buffers + for (uint32_t v = 0; v < poly; ++v) { + voices->at(v).buffer.reset(); + voices->at(v).buffer = (bufs.*get)( + buffer_type(), _value.type(), _buffer_size); + voices->at(v).buffer->clear(); + if (_value.is_valid()) { + voices->at(v).buffer->set_value(_value); + } + } + return true; +} + +bool +InputPort::pre_get_buffers(BufferFactory& bufs, + MPtr<Voices>& voices, + uint32_t poly) const +{ + return get_buffers(bufs, &BufferFactory::get_buffer, voices, poly, _num_arcs); +} + +bool +InputPort::setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly) +{ + if (is_a(PortType::ATOM) && !_value.is_valid()) { + poly = 1; + } + + if (_arcs.size() == 1 && !is_a(PortType::ATOM) && !_arcs.front().must_mix()) { + // Single non-mixing connection, use buffers directly + for (uint32_t v = 0; v < poly; ++v) { + _voices->at(v).buffer = _arcs.front().buffer(v); + } + return false; + } + + return get_buffers(bufs, &BufferFactory::claim_buffer, _voices, poly, _arcs.size()); +} + +void +InputPort::add_arc(RunContext& context, ArcImpl& c) +{ + _arcs.push_front(c); +} + +void +InputPort::remove_arc(ArcImpl& arc) +{ + _arcs.erase(_arcs.iterator_to(arc)); +} + +uint32_t +InputPort::max_tail_poly(RunContext& context) const +{ + return parent_block()->parent_graph()->internal_poly_process(); +} + +void +InputPort::pre_process(RunContext& context) +{ + if (_arcs.empty()) { + // No incoming arcs, just handle user-set value + for (uint32_t v = 0; v < _poly; ++v) { + // Update set state + update_set_state(context, v); + + // Prepare for write in case a set event executes this cycle + if (!_parent->is_main()) { + buffer(v)->prepare_write(context); + } + } + } else if (direct_connect()) { + // Directly connected, use source's buffer directly + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer = _arcs.front().buffer(v); + } + } else { + // Mix down to local buffers in pre_run() + for (uint32_t v = 0; v < _poly; ++v) { + buffer(v)->prepare_write(context); + } + } +} + +void +InputPort::pre_run(RunContext& context) +{ + if ((_user_buffer || !_arcs.empty()) && !direct_connect()) { + const uint32_t src_poly = max_tail_poly(context); + const uint32_t max_n_srcs = _arcs.size() * src_poly + 1; + + for (uint32_t v = 0; v < _poly; ++v) { + if (!buffer(v)->get<void>()) { + continue; + } + + // Get all sources for this voice + const Buffer* srcs[max_n_srcs]; + uint32_t n_srcs = 0; + + if (_user_buffer) { + // Add buffer with user/UI input for this cycle + srcs[n_srcs++] = _user_buffer.get(); + } + + for (const auto& arc : _arcs) { + if (_poly == 1) { + // P -> 1 or 1 -> 1: all tail voices => each head voice + for (uint32_t w = 0; w < arc.tail()->poly(); ++w) { + assert(n_srcs < max_n_srcs); + srcs[n_srcs++] = arc.buffer(w, context.offset()).get(); + assert(srcs[n_srcs - 1]); + } + } else { + // P -> P or 1 -> P: tail voice => corresponding head voice + assert(n_srcs < max_n_srcs); + srcs[n_srcs++] = arc.buffer(v, context.offset()).get(); + assert(srcs[n_srcs - 1]); + } + } + + // Then mix them into our buffer for this voice + mix(context, buffer(v).get(), srcs, n_srcs); + update_values(context.offset(), v); + } + } else if (is_a(PortType::CONTROL)) { + for (uint32_t v = 0; v < _poly; ++v) { + update_values(context.offset(), v); + } + } +} + +SampleCount +InputPort::next_value_offset(SampleCount offset, SampleCount end) const +{ + SampleCount earliest = end; + + if (_user_buffer) { + earliest = _user_buffer->next_value_offset(offset, end); + } + + for (const auto& arc : _arcs) { + const SampleCount o = arc.tail()->next_value_offset(offset, end); + if (o < earliest) { + earliest = o; + } + } + + return earliest; +} + +void +InputPort::post_process(RunContext& context) +{ + if (!_arcs.empty() || _force_monitor_update) { + monitor(context, _force_monitor_update); + _force_monitor_update = false; + } + + /* Finished processing any user/UI messages for this cycle, drop reference + to user buffer. */ + _user_buffer.reset(); +} + +bool +InputPort::direct_connect() const +{ + return _arcs.size() == 1 + && !_parent->is_main() + && !_arcs.front().must_mix() + && buffer(0)->type() != _bufs.uris().atom_Sequence; +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/InputPort.hpp b/src/server/InputPort.hpp new file mode 100644 index 00000000..708f7ea2 --- /dev/null +++ b/src/server/InputPort.hpp @@ -0,0 +1,128 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_INPUTPORT_HPP +#define INGEN_ENGINE_INPUTPORT_HPP + +#include <cassert> +#include <cstdlib> + +#include <boost/intrusive/slist.hpp> + +#include "ingen/types.hpp" + +#include "ArcImpl.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +class ArcImpl; +class BlockImpl; +class RunContext; + +/** An input port on a Block or Graph. + * + * All ports have a Buffer, but the actual contents (data) of that buffer may be + * set directly to the incoming arc's buffer if there's only one inbound + * arc, to eliminate the need to copy/mix. + * + * If a port has multiple arcs, they will be mixed down into the local + * buffer and it will be used. + * + * \ingroup engine + */ +class InputPort : public PortImpl +{ +public: + InputPort(BufferFactory& bufs, + BlockImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size = 0); + + typedef boost::intrusive::slist<ArcImpl, + boost::intrusive::constant_time_size<true> + > Arcs; + + /** Return the maximum polyphony of an output connected to this input. */ + virtual uint32_t max_tail_poly(RunContext& context) const; + + bool apply_poly(RunContext& context, uint32_t poly); + + /** Add an arc. Realtime safe. + * + * The buffer of this port will be set directly to the arc's buffer + * if there is only one arc, since no copying/mixing needs to take place. + * + * setup_buffers() must be called later for the change to take effect. + */ + void add_arc(RunContext& context, ArcImpl& c); + + /** Remove an arc. Realtime safe. + * + * setup_buffers() must be called later for the change to take effect. + */ + void remove_arc(ArcImpl& arc); + + /** Like `get_buffers`, but for the pre-process thread. + * + * This uses the "current" number of arcs fromthe perspective of the + * pre-process thread to allocate buffers for application of a + * connection/disconnection/etc in the next process cycle. + */ + bool pre_get_buffers(BufferFactory& bufs, + MPtr<Voices>& voices, + uint32_t poly) const; + + bool setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly); + + /** Set up buffer pointers. */ + void pre_process(RunContext& context); + + /** Prepare buffer for access, mixing if necessary. */ + void pre_run(RunContext& context); + + /** Prepare buffer for next process cycle. */ + void post_process(RunContext& context); + + SampleCount next_value_offset(SampleCount offset, SampleCount end) const; + + size_t num_arcs() const { return _num_arcs; } + void increment_num_arcs() { ++_num_arcs; } + void decrement_num_arcs() { --_num_arcs; } + + bool direct_connect() const; + +protected: + bool get_buffers(BufferFactory& bufs, + PortImpl::GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const; + + size_t _num_arcs; ///< Pre-process thread + Arcs _arcs; ///< Audio thread +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_INPUTPORT_HPP diff --git a/src/server/InternalBlock.cpp b/src/server/InternalBlock.cpp new file mode 100644 index 00000000..3d8f7390 --- /dev/null +++ b/src/server/InternalBlock.cpp @@ -0,0 +1,73 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "InternalBlock.hpp" +#include "InternalPlugin.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +InternalBlock::InternalBlock(PluginImpl* plugin, + const Raul::Symbol& symbol, + bool poly, + GraphImpl* parent, + SampleRate rate) + : BlockImpl(plugin, symbol, poly, parent, rate) +{} + +BlockImpl* +InternalBlock::duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) +{ + BufferFactory& bufs = *engine.buffer_factory(); + + BlockImpl* copy = reinterpret_cast<InternalPlugin*>(_plugin)->instantiate( + bufs, symbol, _polyphonic, parent_graph(), engine, nullptr); + + for (size_t i = 0; i < num_ports(); ++i) { + const Atom& value = port_impl(i)->value(); + copy->port_impl(i)->set_property(bufs.uris().ingen_value, value); + copy->port_impl(i)->set_value(value); + } + + return copy; +} + +void +InternalBlock::pre_process(RunContext& context) +{ + for (uint32_t i = 0; i < num_ports(); ++i) { + PortImpl* const port = _ports->at(i); + if (port->is_input()) { + port->pre_process(context); + } else if (port->buffer_type() == _plugin->uris().atom_Sequence) { + /* Output sequences are initialized in LV2 format, an atom:Chunk + with size set to the capacity of the buffer. Internal nodes + don't care, so clear to an empty sequences so appending events + results in a valid output. */ + for (uint32_t v = 0; v < port->poly(); ++v) { + port->buffer(v)->clear(); + } + } + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/InternalBlock.hpp b/src/server/InternalBlock.hpp new file mode 100644 index 00000000..a57bd89f --- /dev/null +++ b/src/server/InternalBlock.hpp @@ -0,0 +1,48 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_INTERNALBLOCK_HPP +#define INGEN_ENGINE_INTERNALBLOCK_HPP + +#include "BlockImpl.hpp" + +namespace Ingen { +namespace Server { + +/** An internal Block implemented inside Ingen. + * + * \ingroup engine + */ +class InternalBlock : public BlockImpl +{ +public: + InternalBlock(PluginImpl* plugin, + const Raul::Symbol& symbol, + bool poly, + GraphImpl* parent, + SampleRate rate); + + BlockImpl* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent); + + virtual void pre_process(RunContext& context); +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_BLOCKIMPL_HPP diff --git a/src/server/InternalPlugin.cpp b/src/server/InternalPlugin.cpp new file mode 100644 index 00000000..6529b9c0 --- /dev/null +++ b/src/server/InternalPlugin.cpp @@ -0,0 +1,67 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" +#include "internals/Controller.hpp" +#include "internals/BlockDelay.hpp" +#include "internals/Note.hpp" +#include "internals/Time.hpp" +#include "internals/Trigger.hpp" + +#include "Engine.hpp" +#include "InternalPlugin.hpp" + +namespace Ingen { +namespace Server { + +using namespace Internals; + +InternalPlugin::InternalPlugin(URIs& uris, + const URI& uri, + const Raul::Symbol& symbol) + : PluginImpl(uris, uris.ingen_Internal.urid, uri) + , _symbol(symbol) +{ + set_property(uris.rdf_type, uris.ingen_Internal); +} + +BlockImpl* +InternalPlugin::instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state) +{ + const SampleCount srate = engine.sample_rate(); + + if (uri() == NS_INTERNALS "BlockDelay") { + return new BlockDelayNode(this, bufs, symbol, polyphonic, parent, srate); + } else if (uri() == NS_INTERNALS "Controller") { + return new ControllerNode(this, bufs, symbol, polyphonic, parent, srate); + } else if (uri() == NS_INTERNALS "Note") { + return new NoteNode(this, bufs, symbol, polyphonic, parent, srate); + } else if (uri() == NS_INTERNALS "Time") { + return new TimeNode(this, bufs, symbol, polyphonic, parent, srate); + } else if (uri() == NS_INTERNALS "Trigger") { + return new TriggerNode(this, bufs, symbol, polyphonic, parent, srate); + } else { + return nullptr; + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/InternalPlugin.hpp b/src/server/InternalPlugin.hpp new file mode 100644 index 00000000..79309beb --- /dev/null +++ b/src/server/InternalPlugin.hpp @@ -0,0 +1,57 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_INTERNALPLUGIN_HPP +#define INGEN_ENGINE_INTERNALPLUGIN_HPP + +#include "raul/Symbol.hpp" + +#include "PluginImpl.hpp" + +#define NS_INTERNALS "http://drobilla.net/ns/ingen-internals#" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class BufferFactory; + +/** Implementation of an Internal plugin. + */ +class InternalPlugin : public PluginImpl +{ +public: + InternalPlugin(URIs& uris, + const URI& uri, + const Raul::Symbol& symbol); + + BlockImpl* instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state); + + const Raul::Symbol symbol() const { return _symbol; } + +private: + const Raul::Symbol _symbol; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_INTERNALPLUGIN_HPP diff --git a/src/server/JackDriver.cpp b/src/server/JackDriver.cpp new file mode 100644 index 00000000..973e3eb7 --- /dev/null +++ b/src/server/JackDriver.cpp @@ -0,0 +1,584 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen_config.h" + +#include <cstdlib> +#include <string> + +#include <jack/midiport.h> +#ifdef INGEN_JACK_SESSION +#include <jack/session.h> +#include <boost/format.hpp> +#include "ingen/Serialiser.hpp" +#endif +#ifdef HAVE_JACK_METADATA +#include <jack/metadata.h> +#include "jackey.h" +#endif + +#include "ingen/Configuration.hpp" +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "Buffer.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "JackDriver.hpp" +#include "PortImpl.hpp" +#include "ThreadManager.hpp" +#include "util.hpp" + +typedef jack_default_audio_sample_t jack_sample_t; + +namespace Ingen { +namespace Server { + +JackDriver::JackDriver(Engine& engine) + : _engine(engine) + , _sem(0) + , _flag(false) + , _client(nullptr) + , _block_length(0) + , _seq_size(0) + , _sample_rate(0) + , _is_activated(false) + , _old_bpm(120.0f) + , _old_frame(0) + , _old_rolling(false) +{ + _midi_event_type = _engine.world()->uris().midi_MidiEvent; + lv2_atom_forge_init( + &_forge, &engine.world()->uri_map().urid_map_feature()->urid_map); +} + +JackDriver::~JackDriver() +{ + deactivate(); + _ports.clear_and_dispose([](EnginePort* p) { delete p; }); +} + +bool +JackDriver::attach(const std::string& server_name, + const std::string& client_name, + void* jack_client) +{ + assert(!_client); + if (!jack_client) { +#ifdef INGEN_JACK_SESSION + const std::string uuid = _engine.world()->jack_uuid(); + if (!uuid.empty()) { + _client = jack_client_open(client_name.c_str(), + JackSessionID, nullptr, + uuid.c_str()); + _engine.log().info(fmt("Connected to Jack as `%1%' (UUID `%2%')\n") + % client_name.c_str() % uuid); + } +#endif + + // Try supplied server name + if (!_client && !server_name.empty()) { + if ((_client = jack_client_open(client_name.c_str(), + JackServerName, nullptr, + server_name.c_str()))) { + _engine.log().info(fmt("Connected to Jack server `%1%'\n") + % server_name); + } + } + + // Either server name not specified, or supplied server name does not exist + // Connect to default server + if (!_client) { + if ((_client = jack_client_open(client_name.c_str(), JackNullOption, nullptr))) { + _engine.log().info("Connected to default Jack server\n"); + } + } + + // Still failed + if (!_client) { + _engine.log().error("Unable to connect to Jack\n"); + return false; + } + } else { + _client = (jack_client_t*)jack_client; + } + + _sample_rate = jack_get_sample_rate(_client); + _block_length = jack_get_buffer_size(_client); + _seq_size = jack_port_type_get_buffer_size(_client, JACK_DEFAULT_MIDI_TYPE); + + _fallback_buffer = AudioBufPtr( + static_cast<float*>( + Buffer::aligned_alloc(sizeof(float) * _block_length))); + + jack_on_shutdown(_client, shutdown_cb, this); + + jack_set_thread_init_callback(_client, thread_init_cb, this); + jack_set_buffer_size_callback(_client, block_length_cb, this); +#ifdef INGEN_JACK_SESSION + jack_set_session_callback(_client, session_cb, this); +#endif + + for (auto& p : _ports) { + register_port(p); + } + + return true; +} + +bool +JackDriver::activate() +{ + World* world = _engine.world(); + + if (_is_activated) { + _engine.log().warn("Jack driver already activated\n"); + return false; + } + + if (!_client) { + attach(world->conf().option("jack-server").ptr<char>(), + world->conf().option("jack-name").ptr<char>(), nullptr); + } + + if (!_client) { + return false; + } + + jack_set_process_callback(_client, process_cb, this); + + _is_activated = true; + + if (jack_activate(_client)) { + _engine.log().error("Could not activate Jack client, aborting\n"); + return false; + } else { + _engine.log().info(fmt("Activated Jack client `%1%'\n") % + world->conf().option("jack-name").ptr<char>()); + } + return true; +} + +void +JackDriver::deactivate() +{ + if (_is_activated) { + _flag = true; + _is_activated = false; + _sem.timed_wait(std::chrono::seconds(1)); + + for (auto& p : _ports) { + unregister_port(p); + } + + if (_client) { + jack_deactivate(_client); + jack_client_close(_client); + _client = nullptr; + } + + _engine.log().info("Deactivated Jack client\n"); + } +} + +EnginePort* +JackDriver::get_port(const Raul::Path& path) +{ + for (auto& p : _ports) { + if (p.graph_port()->path() == path) { + return &p; + } + } + + return nullptr; +} + +void +JackDriver::add_port(RunContext& context, EnginePort* port) +{ + _ports.push_back(*port); + + DuplexPort* graph_port = port->graph_port(); + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + const SampleCount nframes = context.nframes(); + jack_port_t* jport = (jack_port_t*)port->handle(); + void* jbuf = jack_port_get_buffer(jport, nframes); + + /* Jack fails to return a buffer if this is too soon after registering + the port, so use a silent fallback buffer if necessary. */ + graph_port->set_driver_buffer( + jbuf ? jbuf : _fallback_buffer.get(), + nframes * sizeof(float)); + } +} + +void +JackDriver::remove_port(RunContext& context, EnginePort* port) +{ + _ports.erase(_ports.iterator_to(*port)); +} + +void +JackDriver::register_port(EnginePort& port) +{ + jack_port_t* jack_port = jack_port_register( + _client, + port.graph_port()->path().substr(1).c_str(), + ((port.graph_port()->is_a(PortType::AUDIO) || + port.graph_port()->is_a(PortType::CV)) + ? JACK_DEFAULT_AUDIO_TYPE : JACK_DEFAULT_MIDI_TYPE), + (port.graph_port()->is_input() + ? JackPortIsInput : JackPortIsOutput), + 0); + + if (!jack_port) { + throw JackDriver::PortRegistrationFailedException(); + } + + port.set_handle(jack_port); + + for (const auto& p : port.graph_port()->properties()) { + port_property_internal(jack_port, p.first, p.second); + } +} + +void +JackDriver::unregister_port(EnginePort& port) +{ + if (jack_port_unregister(_client, (jack_port_t*)port.handle())) { + _engine.log().error("Failed to unregister Jack port\n"); + } + + port.set_handle(nullptr); +} + +void +JackDriver::rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) +{ + EnginePort* eport = get_port(old_path); + if (eport) { +#ifdef HAVE_JACK_PORT_RENAME + jack_port_rename( + _client, (jack_port_t*)eport->handle(), new_path.substr(1).c_str()); +#else + jack_port_set_name((jack_port_t*)eport->handle(), + new_path.substr(1).c_str()); +#endif + } +} + +void +JackDriver::port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) +{ +#ifdef HAVE_JACK_METADATA + EnginePort* eport = get_port(path); + if (eport) { + const jack_port_t* const jport = (const jack_port_t*)eport->handle(); + port_property_internal(jport, uri, value); + } +#endif +} + +void +JackDriver::port_property_internal(const jack_port_t* jport, + const URI& uri, + const Atom& value) +{ +#ifdef HAVE_JACK_METADATA + if (uri == _engine.world()->uris().lv2_name) { + jack_set_property(_client, jack_port_uuid(jport), + JACK_METADATA_PRETTY_NAME, value.ptr<char>(), "text/plain"); + } else if (uri == _engine.world()->uris().lv2_index) { + jack_set_property(_client, jack_port_uuid(jport), + JACKEY_ORDER, std::to_string(value.get<int32_t>()).c_str(), + "http://www.w3.org/2001/XMLSchema#integer"); + } else if (uri == _engine.world()->uris().rdf_type) { + if (value == _engine.world()->uris().lv2_CVPort) { + jack_set_property(_client, jack_port_uuid(jport), + JACKEY_SIGNAL_TYPE, "CV", "text/plain"); + } + } +#endif +} + +EnginePort* +JackDriver::create_port(DuplexPort* graph_port) +{ + EnginePort* eport = nullptr; + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + // Audio buffer port, use Jack buffer directly + eport = new EnginePort(graph_port); + graph_port->set_is_driver_port(*_engine.buffer_factory()); + } else if (graph_port->is_a(PortType::ATOM) && + graph_port->buffer_type() == _engine.world()->uris().atom_Sequence) { + // Sequence port, make Jack port but use internal LV2 format buffer + eport = new EnginePort(graph_port); + } + + if (eport) { + register_port(*eport); + } + + return eport; +} + +void +JackDriver::pre_process_port(RunContext& context, EnginePort* port) +{ + const URIs& uris = context.engine().world()->uris(); + const SampleCount nframes = context.nframes(); + jack_port_t* jack_port = (jack_port_t*)port->handle(); + DuplexPort* graph_port = port->graph_port(); + Buffer* graph_buf = graph_port->buffer(0).get(); + void* jack_buf = jack_port_get_buffer(jack_port, nframes); + + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + graph_port->set_driver_buffer(jack_buf, nframes * sizeof(float)); + if (graph_port->is_input()) { + graph_port->monitor(context); + } else { + graph_port->buffer(0)->clear(); // TODO: Avoid when possible + } + } else if (graph_port->buffer_type() == uris.atom_Sequence) { + graph_buf->prepare_write(context); + if (graph_port->is_input()) { + // Copy events from Jack port buffer into graph port buffer + const jack_nframes_t event_count = jack_midi_get_event_count(jack_buf); + for (jack_nframes_t i = 0; i < event_count; ++i) { + jack_midi_event_t ev; + jack_midi_event_get(&ev, jack_buf, i); + if (!graph_buf->append_event( + ev.time, ev.size, _midi_event_type, ev.buffer)) { + _engine.log().rt_error("Failed to write to MIDI buffer, events lost!\n"); + } + } + } + graph_port->monitor(context); + } +} + +void +JackDriver::post_process_port(RunContext& context, EnginePort* port) +{ + const URIs& uris = context.engine().world()->uris(); + const SampleCount nframes = context.nframes(); + jack_port_t* jack_port = (jack_port_t*)port->handle(); + DuplexPort* graph_port = port->graph_port(); + void* jack_buf = port->buffer(); + + if (port->graph_port()->is_output()) { + if (!jack_buf) { + // First cycle for a new output, so pre_process wasn't called + jack_buf = jack_port_get_buffer(jack_port, nframes); + port->set_buffer(jack_buf); + } + + if (graph_port->buffer_type() == uris.atom_Sequence) { + // Copy LV2 MIDI events to Jack MIDI buffer + Buffer* const graph_buf = graph_port->buffer(0).get(); + LV2_Atom_Sequence* seq = graph_buf->get<LV2_Atom_Sequence>(); + + jack_midi_clear_buffer(jack_buf); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY(&ev->body); + if (ev->body.type == _midi_event_type) { + jack_midi_event_write( + jack_buf, ev->time.frames, buf, ev->body.size); + } + } + } + } + + // Reset graph port buffer pointer to no longer point to the Jack buffer + if (graph_port->is_driver_port()) { + graph_port->set_driver_buffer(nullptr, 0); + } +} + +void +JackDriver::append_time_events(RunContext& context, + Buffer& buffer) +{ + const URIs& uris = context.engine().world()->uris(); + const jack_position_t* pos = &_position; + const bool rolling = (_transport_state == JackTransportRolling); + + // Do nothing if there is no unexpected time change (other than rolling) + if (rolling == _old_rolling && + pos->frame == _old_frame && + pos->beats_per_minute == _old_bpm) { + return; + } + + // Update old time information to detect change next cycle + _old_frame = pos->frame; + _old_rolling = rolling; + _old_bpm = pos->beats_per_minute; + + // Build an LV2 position object to append to the buffer + LV2_Atom pos_buf[16]; + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_set_buffer(&_forge, (uint8_t*)pos_buf, sizeof(pos_buf)); + lv2_atom_forge_object(&_forge, &frame, 0, uris.time_Position); + lv2_atom_forge_key(&_forge, uris.time_frame); + lv2_atom_forge_long(&_forge, pos->frame); + lv2_atom_forge_key(&_forge, uris.time_speed); + lv2_atom_forge_float(&_forge, rolling ? 1.0 : 0.0); + if (pos->valid & JackPositionBBT) { + lv2_atom_forge_key(&_forge, uris.time_barBeat); + lv2_atom_forge_float( + &_forge, pos->beat - 1 + (pos->tick / pos->ticks_per_beat)); + lv2_atom_forge_key(&_forge, uris.time_bar); + lv2_atom_forge_long(&_forge, pos->bar - 1); + lv2_atom_forge_key(&_forge, uris.time_beatUnit); + lv2_atom_forge_int(&_forge, pos->beat_type); + lv2_atom_forge_key(&_forge, uris.time_beatsPerBar); + lv2_atom_forge_float(&_forge, pos->beats_per_bar); + lv2_atom_forge_key(&_forge, uris.time_beatsPerMinute); + lv2_atom_forge_float(&_forge, pos->beats_per_minute); + } + + // Append position to buffer at offset 0 (start of this cycle) + LV2_Atom* lpos = (LV2_Atom*)pos_buf; + buffer.append_event( + 0, lpos->size, lpos->type, (const uint8_t*)LV2_ATOM_BODY_CONST(lpos)); +} + +/**** Jack Callbacks ****/ + +/** Jack process callback, drives entire audio thread. + * + * \callgraph + */ +REALTIME int +JackDriver::_process_cb(jack_nframes_t nframes) +{ + if (nframes == 0 || ! _is_activated) { + if (_flag) { + _sem.post(); + } + return 0; + } + + /* Note that Jack may not call this function for a cycle, if overloaded, + so a rolling counter here would not always be correct. */ + const jack_nframes_t start_of_current_cycle = jack_last_frame_time(_client); + + _transport_state = jack_transport_query(_client, &_position); + + _engine.locate(start_of_current_cycle, nframes); + + // Read input + for (auto& p : _ports) { + pre_process_port(_engine.run_context(), &p); + } + + // Process + _engine.run(nframes); + + // Write output + for (auto& p : _ports) { + post_process_port(_engine.run_context(), &p); + } + + // Update expected transport frame for next cycle to detect changes + if (_transport_state == JackTransportRolling) { + _old_frame += nframes; + } + + return 0; +} + +void +JackDriver::_thread_init_cb() +{ + ThreadManager::set_flag(THREAD_PROCESS); + ThreadManager::set_flag(THREAD_IS_REAL_TIME); +} + +void +JackDriver::_shutdown_cb() +{ + _engine.log().info("Jack shutdown, exiting\n"); + _is_activated = false; + _client = nullptr; +} + +int +JackDriver::_block_length_cb(jack_nframes_t nframes) +{ + if (_engine.root_graph()) { + _block_length = nframes; + _seq_size = jack_port_type_get_buffer_size(_client, JACK_DEFAULT_MIDI_TYPE); + _engine.root_graph()->set_buffer_size( + _engine.run_context(), *_engine.buffer_factory(), PortType::AUDIO, + _engine.buffer_factory()->audio_buffer_size(nframes)); + _engine.root_graph()->set_buffer_size( + _engine.run_context(), *_engine.buffer_factory(), PortType::ATOM, + _seq_size); + } + return 0; +} + +#ifdef INGEN_JACK_SESSION +void +JackDriver::_session_cb(jack_session_event_t* event) +{ + _engine.log().info(fmt("Jack session save to %1%\n") % event->session_dir); + + const std::string cmd = ( + boost::format("ingen -eg -n %1% -u %2% -l ${SESSION_DIR}") + % jack_get_client_name(_client) + % event->client_uuid).str(); + + SPtr<Serialiser> serialiser = _engine.world()->serialiser(); + if (serialiser) { + std::lock_guard<std::mutex> lock(_engine.world()->rdf_mutex()); + + SPtr<Node> root(_engine.root_graph(), NullDeleter<Node>); + serialiser->write_bundle(root, + URI(std::string("file://") + event->session_dir)); + } + + event->command_line = (char*)malloc(cmd.size() + 1); + memcpy(event->command_line, cmd.c_str(), cmd.size() + 1); + jack_session_reply(_client, event); + + switch (event->type) { + case JackSessionSave: + break; + case JackSessionSaveAndQuit: + _engine.log().warn("Jack session quit\n"); + _engine.quit(); + break; + case JackSessionSaveTemplate: + break; + } + + jack_session_event_free(event); +} +#endif + +} // namespace Server +} // namespace Ingen diff --git a/src/server/JackDriver.hpp b/src/server/JackDriver.hpp new file mode 100644 index 00000000..2a21d96e --- /dev/null +++ b/src/server/JackDriver.hpp @@ -0,0 +1,169 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_JACKAUDIODRIVER_HPP +#define INGEN_ENGINE_JACKAUDIODRIVER_HPP + +#include "ingen_config.h" + +#include <string> +#include <atomic> + +#include <jack/jack.h> +#include <jack/thread.h> +#include <jack/transport.h> +#ifdef INGEN_JACK_SESSION +#include <jack/session.h> +#endif + +#include "ingen/types.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "raul/Semaphore.hpp" + +#include "Driver.hpp" +#include "EnginePort.hpp" + +namespace Raul { class Path; } + +namespace Ingen { +namespace Server { + +class DuplexPort; +class Engine; +class GraphImpl; +class JackDriver; +class PortImpl; + +/** The Jack Driver. + * + * The process callback here drives the entire audio thread by "pulling" + * events from queues, processing them, running the graphs, and passing + * events along to the PostProcessor. + * + * \ingroup engine + */ +class JackDriver : public Driver +{ +public: + explicit JackDriver(Engine& engine); + ~JackDriver(); + + bool attach(const std::string& server_name, + const std::string& client_name, + void* jack_client); + + bool activate(); + void deactivate(); + + bool dynamic_ports() const { return true; } + + EnginePort* create_port(DuplexPort* graph_port); + EnginePort* get_port(const Raul::Path& path); + + void rename_port(const Raul::Path& old_path, const Raul::Path& new_path); + void port_property(const Raul::Path& path, const URI& uri, const Atom& value); + void add_port(RunContext& context, EnginePort* port); + void remove_port(RunContext& context, EnginePort* port); + void register_port(EnginePort& port); + void unregister_port(EnginePort& port); + + /** Transport state for this frame. + * Intended to only be called from the audio thread. */ + inline const jack_position_t* position() { return &_position; } + inline jack_transport_state_t transport_state() { return _transport_state; } + + void append_time_events(RunContext& context, + Buffer& buffer); + + int real_time_priority() { return jack_client_real_time_priority(_client); } + + jack_client_t* jack_client() const { return _client; } + SampleCount block_length() const { return _block_length; } + size_t seq_size() const { return _seq_size; } + SampleCount sample_rate() const { return _sample_rate; } + + inline SampleCount frame_time() const { return _client ? jack_frame_time(_client) : 0; } + + class PortRegistrationFailedException : public std::exception {}; + +private: + friend class JackPort; + + // Static JACK callbacks which call the non-static callbacks (methods) + inline static void thread_init_cb(void* const jack_driver) { + return ((JackDriver*)jack_driver)->_thread_init_cb(); + } + inline static void shutdown_cb(void* const jack_driver) { + return ((JackDriver*)jack_driver)->_shutdown_cb(); + } + inline static int process_cb(jack_nframes_t nframes, void* const jack_driver) { + return ((JackDriver*)jack_driver)->_process_cb(nframes); + } + inline static int block_length_cb(jack_nframes_t nframes, void* const jack_driver) { + return ((JackDriver*)jack_driver)->_block_length_cb(nframes); + } +#ifdef INGEN_JACK_SESSION + inline static void session_cb(jack_session_event_t* event, void* jack_driver) { + ((JackDriver*)jack_driver)->_session_cb(event); + } +#endif + + void pre_process_port(RunContext& context, EnginePort* port); + void post_process_port(RunContext& context, EnginePort* port); + + void port_property_internal(const jack_port_t* jport, + const URI& uri, + const Atom& value); + + // Non static callbacks (methods) + void _thread_init_cb(); + void _shutdown_cb(); + int _process_cb(jack_nframes_t nframes); + int _block_length_cb(jack_nframes_t nframes); +#ifdef INGEN_JACK_SESSION + void _session_cb(jack_session_event_t* event); +#endif + +protected: + typedef boost::intrusive::slist<EnginePort, + boost::intrusive::cache_last<true> + > Ports; + + using AudioBufPtr = UPtr<float, FreeDeleter<float>>; + + Engine& _engine; + Ports _ports; + AudioBufPtr _fallback_buffer; + LV2_Atom_Forge _forge; + Raul::Semaphore _sem; + std::atomic<bool> _flag; + jack_client_t* _client; + jack_nframes_t _block_length; + size_t _seq_size; + jack_nframes_t _sample_rate; + uint32_t _midi_event_type; + bool _is_activated; + jack_position_t _position; + jack_transport_state_t _transport_state; + float _old_bpm; + jack_nframes_t _old_frame; + bool _old_rolling; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_JACKAUDIODRIVER_HPP diff --git a/src/server/LV2Block.cpp b/src/server/LV2Block.cpp new file mode 100644 index 00000000..f4792f39 --- /dev/null +++ b/src/server/LV2Block.cpp @@ -0,0 +1,742 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> +#include <cmath> +#include <cstdint> + +#include "lv2/lv2plug.in/ns/ext/morph/morph.h" +#include "lv2/lv2plug.in/ns/ext/presets/presets.h" +#include "lv2/lv2plug.in/ns/ext/options/options.h" +#include "lv2/lv2plug.in/ns/ext/resize-port/resize-port.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" + +#include "raul/Maid.hpp" +#include "raul/Array.hpp" + +#include "ingen/FilePath.hpp" +#include "ingen/Log.hpp" +#include "ingen/URI.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "LV2Block.hpp" +#include "LV2Plugin.hpp" +#include "OutputPort.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "Worker.hpp" + +namespace Ingen { +namespace Server { + +/** Partially construct a LV2Block. + * + * Object is not usable until instantiate() is called with success. + * (It _will_ crash!) + */ +LV2Block::LV2Block(LV2Plugin* plugin, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : BlockImpl(plugin, symbol, polyphonic, parent, srate) + , _lv2_plugin(plugin) + , _worker_iface(nullptr) +{ + assert(_lv2_plugin); +} + +LV2Block::~LV2Block() +{ + // Explicitly drop instances first to prevent reference cycles + drop_instances(_instances); + drop_instances(_prepared_instances); +} + +SPtr<LV2Block::Instance> +LV2Block::make_instance(URIs& uris, + SampleRate rate, + uint32_t voice, + bool preparing) +{ + const Engine& engine = parent_graph()->engine(); + const LilvPlugin* lplug = _lv2_plugin->lilv_plugin(); + LilvInstance* inst = lilv_plugin_instantiate( + lplug, rate, _features->array()); + + if (!inst) { + engine.log().error(fmt("Failed to instantiate <%1%>\n") + % _lv2_plugin->uri().c_str()); + return SPtr<Instance>(); + } + + const LV2_Options_Interface* options_iface = nullptr; + if (lilv_plugin_has_extension_data(lplug, uris.opt_interface)) { + options_iface = (const LV2_Options_Interface*) + lilv_instance_get_extension_data(inst, LV2_OPTIONS__interface); + } + + for (uint32_t p = 0; p < num_ports(); ++p) { + PortImpl* const port = _ports->at(p); + Buffer* const buffer = (preparing) + ? port->prepared_buffer(voice).get() + : port->buffer(voice).get(); + if (port->is_morph() && port->is_a(PortType::CV)) { + if (options_iface) { + const LV2_URID port_type = uris.lv2_CVPort; + const LV2_Options_Option options[] = { + { LV2_OPTIONS_PORT, p, uris.morph_currentType, + sizeof(LV2_URID), uris.atom_URID, &port_type }, + { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr } + }; + options_iface->set(inst->lv2_handle, options); + } + } + + if (buffer) { + if (port->is_a(PortType::CONTROL)) { + buffer->set_value(port->value()); + } else if (port->is_a(PortType::CV)) { + buffer->set_block(port->value().get<float>(), 0, engine.block_length()); + } else { + buffer->clear(); + } + } + } + + if (options_iface) { + for (uint32_t p = 0; p < num_ports(); ++p) { + PortImpl* const port = _ports->at(p); + if (port->is_auto_morph()) { + LV2_Options_Option options[] = { + { LV2_OPTIONS_PORT, p, uris.morph_currentType, 0, 0, nullptr }, + { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr } + }; + + options_iface->get(inst->lv2_handle, options); + if (options[0].value) { + LV2_URID type = *(const LV2_URID*)options[0].value; + if (type == _uris.lv2_ControlPort) { + port->set_type(PortType::CONTROL, 0); + } else if (type == _uris.lv2_CVPort) { + port->set_type(PortType::CV, 0); + } else { + parent_graph()->engine().log().error( + fmt("%1% auto-morphed to unknown type %2%\n") + % port->path().c_str() % type); + return SPtr<Instance>(); + } + } else { + parent_graph()->engine().log().error( + fmt("Failed to get auto-morphed type of %1%\n") + % port->path().c_str()); + } + } + } + } + + return std::make_shared<Instance>(inst); +} + +bool +LV2Block::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + if (!_polyphonic) { + poly = 1; + } + + BlockImpl::prepare_poly(bufs, poly); + + if (_polyphony == poly) { + return true; + } + + const SampleRate rate = bufs.engine().sample_rate(); + assert(!_prepared_instances); + _prepared_instances = bufs.maid().make_managed<Instances>( + poly, *_instances, SPtr<Instance>()); + for (uint32_t i = _polyphony; i < _prepared_instances->size(); ++i) { + SPtr<Instance> inst = make_instance(bufs.uris(), rate, i, true); + if (!inst) { + _prepared_instances.reset(); + return false; + } + + _prepared_instances->at(i) = inst; + + if (_activated) { + lilv_instance_activate(inst->instance); + } + } + + return true; +} + +bool +LV2Block::apply_poly(RunContext& context, uint32_t poly) +{ + if (!_polyphonic) { + poly = 1; + } + + if (_prepared_instances) { + _instances = std::move(_prepared_instances); + } + assert(poly <= _instances->size()); + + return BlockImpl::apply_poly(context, poly); +} + +/** Instantiate self from LV2 plugin descriptor. + * + * Implemented as a seperate function (rather than in the constructor) to + * allow graceful error-catching of broken plugins. + * + * Returns whether or not plugin was successfully instantiated. If return + * value is false, this object may not be used. + */ +bool +LV2Block::instantiate(BufferFactory& bufs, const LilvState* state) +{ + const Ingen::URIs& uris = bufs.uris(); + Ingen::World* world = bufs.engine().world(); + const LilvPlugin* plug = _lv2_plugin->lilv_plugin(); + Ingen::Forge& forge = bufs.forge(); + const uint32_t num_ports = lilv_plugin_get_num_ports(plug); + + LilvNode* lv2_connectionOptional = lilv_new_uri( + bufs.engine().world()->lilv_world(), LV2_CORE__connectionOptional); + + _ports = bufs.maid().make_managed<BlockImpl::Ports>(num_ports, nullptr); + + bool ret = true; + + float* min_values = new float[num_ports]; + float* max_values = new float[num_ports]; + float* def_values = new float[num_ports]; + lilv_plugin_get_port_ranges_float(plug, min_values, max_values, def_values); + uint32_t max_sequence_size = 0; + + // Get all the necessary information about ports + for (uint32_t j = 0; j < num_ports; ++j) { + const LilvPort* id = lilv_plugin_get_port_by_index(plug, j); + + /* LV2 port symbols are guaranteed to be unique, valid C identifiers, + and Lilv guarantees that lilv_port_get_symbol() is valid. */ + const Raul::Symbol port_sym( + lilv_node_as_string(lilv_port_get_symbol(plug, id))); + + // Get port type + Atom val; + PortType port_type = PortType::UNKNOWN; + LV2_URID buffer_type = 0; + bool is_morph = false; + bool is_auto_morph = false; + if (lilv_port_is_a(plug, id, uris.lv2_ControlPort)) { + if (lilv_port_is_a(plug, id, uris.morph_MorphPort)) { + is_morph = true; + LilvNodes* types = lilv_port_get_value( + plug, id, uris.morph_supportsType); + LILV_FOREACH(nodes, i, types) { + const LilvNode* type = lilv_nodes_get(types, i); + if (lilv_node_equals(type, uris.lv2_CVPort)) { + port_type = PortType::CV; + buffer_type = uris.atom_Sound; + } + } + lilv_nodes_free(types); + } + if (port_type == PortType::UNKNOWN) { + port_type = PortType::CONTROL; + buffer_type = uris.atom_Sequence; + val = forge.make(def_values[j]); + } + } else if (lilv_port_is_a(plug, id, uris.lv2_CVPort)) { + port_type = PortType::CV; + buffer_type = uris.atom_Sound; + } else if (lilv_port_is_a(plug, id, uris.lv2_AudioPort)) { + port_type = PortType::AUDIO; + buffer_type = uris.atom_Sound; + } else if (lilv_port_is_a(plug, id, uris.atom_AtomPort)) { + port_type = PortType::ATOM; + } + + if (lilv_port_is_a(plug, id, uris.morph_AutoMorphPort)) { + is_auto_morph = true; + } + + // Get buffer type if necessary (atom ports) + if (!buffer_type) { + LilvNodes* types = lilv_port_get_value( + plug, id, uris.atom_bufferType); + LILV_FOREACH(nodes, i, types) { + const LilvNode* type = lilv_nodes_get(types, i); + if (lilv_node_is_uri(type)) { + buffer_type = bufs.engine().world()->uri_map().map_uri( + lilv_node_as_uri(type)); + } + } + lilv_nodes_free(types); + } + + const bool optional = lilv_port_has_property( + plug, id, lv2_connectionOptional); + + uint32_t port_buffer_size = bufs.default_size(buffer_type); + if (port_buffer_size == 0 && !optional) { + parent_graph()->engine().log().error( + fmt("<%1%> port `%2%' has unknown buffer type\n") + % _lv2_plugin->uri().c_str() % port_sym.c_str()); + ret = false; + break; + } + + if (port_type == PortType::ATOM) { + // Get default value, and its length + LilvNodes* defaults = lilv_port_get_value(plug, id, uris.lv2_default); + LILV_FOREACH(nodes, i, defaults) { + const LilvNode* d = lilv_nodes_get(defaults, i); + if (lilv_node_is_string(d)) { + const char* str_val = lilv_node_as_string(d); + const uint32_t str_val_len = strlen(str_val); + val = forge.alloc(str_val); + port_buffer_size = std::max(port_buffer_size, str_val_len); + } else if (lilv_node_is_uri(d)) { + const char* uri_val = lilv_node_as_uri(d); + val = forge.make_urid( + bufs.engine().world()->uri_map().map_uri(uri_val)); + } + } + lilv_nodes_free(defaults); + + if (!val.type() && buffer_type == _uris.atom_URID) { + val = forge.make_urid(0); + } + + // Get minimum size, if set in data + LilvNodes* sizes = lilv_port_get_value(plug, id, uris.rsz_minimumSize); + LILV_FOREACH(nodes, i, sizes) { + const LilvNode* d = lilv_nodes_get(sizes, i); + if (lilv_node_is_int(d)) { + uint32_t size_val = lilv_node_as_int(d); + port_buffer_size = std::max(port_buffer_size, size_val); + } + } + lilv_nodes_free(sizes); + max_sequence_size = std::max(port_buffer_size, max_sequence_size); + bufs.set_seq_size(max_sequence_size); + } + + enum { UNKNOWN, INPUT, OUTPUT } direction = UNKNOWN; + if (lilv_port_is_a(plug, id, uris.lv2_InputPort)) { + direction = INPUT; + } else if (lilv_port_is_a(plug, id, uris.lv2_OutputPort)) { + direction = OUTPUT; + } + + if ((port_type == PortType::UNKNOWN && !optional) || + direction == UNKNOWN) { + parent_graph()->engine().log().error( + fmt("<%1%> port `%2%' has unknown type or direction\n") + % _lv2_plugin->uri().c_str() % port_sym.c_str()); + ret = false; + break; + } + + if (!val.type() && (port_type != PortType::ATOM)) { + // Ensure numeric ports have a value, use 0 by default + val = forge.make(std::isnan(def_values[j]) ? 0.0f : def_values[j]); + } + + PortImpl* port = (direction == INPUT) + ? static_cast<PortImpl*>( + new InputPort(bufs, this, port_sym, j, _polyphony, + port_type, buffer_type, val)) + : static_cast<PortImpl*>( + new OutputPort(bufs, this, port_sym, j, _polyphony, + port_type, buffer_type, val)); + + port->set_morphable(is_morph, is_auto_morph); + if (direction == INPUT && (port_type == PortType::CONTROL + || port_type == PortType::CV)) { + port->set_value(val); + if (!std::isnan(min_values[j])) { + port->set_minimum(forge.make(min_values[j])); + } + if (!std::isnan(max_values[j])) { + port->set_maximum(forge.make(max_values[j])); + } + } + + // Inherit certain properties from plugin port + const LilvNode* preds[] = { uris.lv2_designation, + uris.lv2_portProperty, + uris.atom_supports, + nullptr }; + for (int p = 0; preds[p]; ++p) { + LilvNodes* values = lilv_port_get_value(plug, id, preds[p]); + LILV_FOREACH(nodes, v, values) { + const LilvNode* val = lilv_nodes_get(values, v); + if (lilv_node_is_uri(val)) { + port->add_property(URI(lilv_node_as_uri(preds[p])), + forge.make_urid(URI(lilv_node_as_uri(val)))); + } + } + lilv_nodes_free(values); + } + + port->cache_properties(); + + _ports->at(j) = port; + } + + delete[] min_values; + delete[] max_values; + delete[] def_values; + + lilv_node_free(lv2_connectionOptional); + + if (!ret) { + _ports.reset(); + return ret; + } + + _features = world->lv2_features().lv2_features(world, this); + + // Actually create plugin instances and port buffers. + const SampleRate rate = bufs.engine().sample_rate(); + _instances = bufs.maid().make_managed<Instances>( + _polyphony, SPtr<Instance>()); + for (uint32_t i = 0; i < _polyphony; ++i) { + _instances->at(i) = make_instance(bufs.uris(), rate, i, false); + if (!_instances->at(i)) { + return false; + } + } + + // Load initial state if no state is explicitly given + LilvState* default_state = nullptr; + if (!state) { + state = default_state = load_preset(_lv2_plugin->uri()); + } + + // Apply state + if (state) { + apply_state(nullptr, state); + } + + if (default_state) { + lilv_state_free(default_state); + } + + // FIXME: Polyphony + worker? + if (lilv_plugin_has_feature(plug, uris.work_schedule)) { + _worker_iface = (const LV2_Worker_Interface*) + lilv_instance_get_extension_data(instance(0), + LV2_WORKER__interface); + } + + return ret; +} + +bool +LV2Block::save_state(const FilePath& dir) const +{ + World* world = _lv2_plugin->world(); + LilvWorld* lworld = world->lilv_world(); + + LilvState* state = lilv_state_new_from_instance( + _lv2_plugin->lilv_plugin(), const_cast<LV2Block*>(this)->instance(0), + &world->uri_map().urid_map_feature()->urid_map, + nullptr, dir.c_str(), dir.c_str(), dir.c_str(), nullptr, nullptr, + LV2_STATE_IS_POD|LV2_STATE_IS_PORTABLE, nullptr); + + if (!state) { + return false; + } else if (lilv_state_get_num_properties(state) == 0) { + lilv_state_free(state); + return false; + } + + lilv_state_save(lworld, + &world->uri_map().urid_map_feature()->urid_map, + &world->uri_map().urid_unmap_feature()->urid_unmap, + state, + nullptr, + dir.c_str(), + "state.ttl"); + + lilv_state_free(state); + + return true; +} + +BlockImpl* +LV2Block::duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent) +{ + const SampleRate rate = engine.sample_rate(); + + // Get current state + LilvState* state = lilv_state_new_from_instance( + _lv2_plugin->lilv_plugin(), instance(0), + &engine.world()->uri_map().urid_map_feature()->urid_map, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, LV2_STATE_IS_NATIVE, nullptr); + + // Duplicate and instantiate block + LV2Block* dup = new LV2Block(_lv2_plugin, symbol, _polyphonic, parent, rate); + if (!dup->instantiate(*engine.buffer_factory(), state)) { + delete dup; + return nullptr; + } + dup->set_properties(properties()); + + // Set duplicate port values and properties to the same as ours + for (uint32_t p = 0; p < num_ports(); ++p) { + const Atom& val = port_impl(p)->value(); + if (val.is_valid()) { + dup->port_impl(p)->set_value(val); + } + dup->port_impl(p)->set_properties(port_impl(p)->properties()); + } + + return dup; +} + +void +LV2Block::activate(BufferFactory& bufs) +{ + BlockImpl::activate(bufs); + + for (uint32_t i = 0; i < _polyphony; ++i) { + lilv_instance_activate(instance(i)); + } +} + +void +LV2Block::deactivate() +{ + BlockImpl::deactivate(); + + for (uint32_t i = 0; i < _polyphony; ++i) { + lilv_instance_deactivate(instance(i)); + } +} + +LV2_Worker_Status +LV2Block::work_respond(LV2_Worker_Respond_Handle handle, + uint32_t size, + const void* data) +{ + LV2Block* block = (LV2Block*)handle; + LV2Block::Response* r = new LV2Block::Response(size, data); + block->_responses.push_back(*r); + return LV2_WORKER_SUCCESS; +} + +LV2_Worker_Status +LV2Block::work(uint32_t size, const void* data) +{ + if (_worker_iface) { + std::lock_guard<std::mutex> lock(_work_mutex); + + LV2_Handle inst = lilv_instance_get_handle(instance(0)); + LV2_Worker_Status st = _worker_iface->work(inst, work_respond, this, size, data); + if (st) { + parent_graph()->engine().log().error( + fmt("Error calling %1% work method\n") % _path); + } + return st; + } + return LV2_WORKER_ERR_UNKNOWN; +} + +void +LV2Block::run(RunContext& context) +{ + for (uint32_t i = 0; i < _polyphony; ++i) { + lilv_instance_run(instance(i), context.nframes()); + } +} + +void +LV2Block::post_process(RunContext& context) +{ + /* Handle any worker responses. Note that this may write to output ports, + so must be done first to prevent clobbering worker responses and + monitored notification ports. */ + if (_worker_iface) { + LV2_Handle inst = lilv_instance_get_handle(instance(0)); + while (!_responses.empty()) { + Response& r = _responses.front(); + _worker_iface->work_response(inst, r.size, r.data); + _responses.pop_front(); + context.engine().maid()->dispose(&r); + } + + if (_worker_iface->end_run) { + _worker_iface->end_run(inst); + } + } + + /* Run cycle truly finished, finalise output ports. */ + BlockImpl::post_process(context); +} + +LilvState* +LV2Block::load_preset(const URI& uri) +{ + World* world = _lv2_plugin->world(); + LilvWorld* lworld = world->lilv_world(); + LilvNode* preset = lilv_new_uri(lworld, uri.c_str()); + + // Load preset into world if necessary + lilv_world_load_resource(lworld, preset); + + // Load preset from world + LV2_URID_Map* map = &world->uri_map().urid_map_feature()->urid_map; + LilvState* state = lilv_state_new_from_world(lworld, map, preset); + + lilv_node_free(preset); + return state; +} + +LilvState* +LV2Block::load_state(World* world, const FilePath& path) +{ + LilvWorld* lworld = world->lilv_world(); + const URI uri = URI(path); + LilvNode* subject = lilv_new_uri(lworld, uri.c_str()); + + LilvState* state = lilv_state_new_from_file( + lworld, + &world->uri_map().urid_map_feature()->urid_map, + subject, + path.c_str()); + + lilv_node_free(subject); + return state; +} + +void +LV2Block::apply_state(const UPtr<Worker>& worker, const LilvState* state) +{ + World* world = parent_graph()->engine().world(); + SPtr<LV2_Feature> sched; + if (worker) { + sched = worker->schedule_feature()->feature(world, this); + } + + const LV2_Feature* state_features[2] = { nullptr, nullptr }; + if (sched) { + state_features[0] = sched.get(); + } + + for (uint32_t v = 0; v < _polyphony; ++v) { + lilv_state_restore(state, instance(v), nullptr, nullptr, 0, state_features); + } +} + +static const void* +get_port_value(const char* port_symbol, + void* user_data, + uint32_t* size, + uint32_t* type) +{ + LV2Block* const block = (LV2Block*)user_data; + PortImpl* const port = block->port_by_symbol(port_symbol); + + if (port && port->is_input() && port->value().is_valid()) { + *size = port->value().size(); + *type = port->value().type(); + return port->value().get_body(); + } + + return nullptr; +} + +boost::optional<Resource> +LV2Block::save_preset(const URI& uri, + const Properties& props) +{ + World* world = parent_graph()->engine().world(); + LilvWorld* lworld = _lv2_plugin->world()->lilv_world(); + LV2_URID_Map* lmap = &world->uri_map().urid_map_feature()->urid_map; + LV2_URID_Unmap* lunmap = &world->uri_map().urid_unmap_feature()->urid_unmap; + + const FilePath path = FilePath(uri.path()); + const FilePath dirname = path.parent_path(); + const FilePath basename = path.stem(); + + LilvState* state = lilv_state_new_from_instance( + _lv2_plugin->lilv_plugin(), instance(0), lmap, + nullptr, nullptr, nullptr, path.c_str(), + get_port_value, this, LV2_STATE_IS_NATIVE, nullptr); + + if (state) { + const Properties::const_iterator l = props.find(_uris.rdfs_label); + if (l != props.end() && l->second.type() == _uris.atom_String) { + lilv_state_set_label(state, l->second.ptr<char>()); + } + + lilv_state_save(lworld, lmap, lunmap, state, nullptr, + dirname.c_str(), basename.c_str()); + + const URI uri(lilv_node_as_uri(lilv_state_get_uri(state))); + const std::string label(lilv_state_get_label(state) + ? lilv_state_get_label(state) + : basename); + lilv_state_free(state); + + Resource preset(_uris, uri); + preset.set_property(_uris.rdf_type, _uris.pset_Preset); + preset.set_property(_uris.rdfs_label, world->forge().alloc(label)); + preset.set_property(_uris.lv2_appliesTo, + world->forge().make_urid(_lv2_plugin->uri())); + + const std::string bundle_uri = URI(dirname).string() + '/'; + LilvNode* lbundle = lilv_new_uri(lworld, bundle_uri.c_str()); + lilv_world_load_bundle(lworld, lbundle); + lilv_node_free(lbundle); + + return preset; + } + + return boost::optional<Resource>(); +} + +void +LV2Block::set_port_buffer(uint32_t voice, + uint32_t port_num, + BufferRef buf, + SampleCount offset) +{ + BlockImpl::set_port_buffer(voice, port_num, buf, offset); + lilv_instance_connect_port( + instance(voice), + port_num, + buf ? buf->port_data(_ports->at(port_num)->type(), offset) : nullptr); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/LV2Block.hpp b/src/server/LV2Block.hpp new file mode 100644 index 00000000..f3a59550 --- /dev/null +++ b/src/server/LV2Block.hpp @@ -0,0 +1,152 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LV2BLOCK_HPP +#define INGEN_ENGINE_LV2BLOCK_HPP + +#include <mutex> + +#include "lilv/lilv.h" +#include "lv2/lv2plug.in/ns/ext/worker/worker.h" +#include "raul/Maid.hpp" + +#include "BufferRef.hpp" +#include "BlockImpl.hpp" +#include "ingen/LV2Features.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class LV2Plugin; + +/** An instance of a LV2 plugin. + * + * \ingroup engine + */ +class LV2Block : public BlockImpl +{ +public: + LV2Block(LV2Plugin* plugin, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + ~LV2Block(); + + bool instantiate(BufferFactory& bufs, const LilvState* state); + + LilvInstance* instance() { return instance(0); } + bool save_state(const FilePath& dir) const; + + BlockImpl* duplicate(Engine& engine, + const Raul::Symbol& symbol, + GraphImpl* parent); + + bool prepare_poly(BufferFactory& bufs, uint32_t poly); + bool apply_poly(RunContext& context, uint32_t poly); + + void activate(BufferFactory& bufs); + void deactivate(); + + LV2_Worker_Status work(uint32_t size, const void* data); + + void run(RunContext& context); + void post_process(RunContext& context); + + LilvState* load_preset(const URI& uri); + + void apply_state(const UPtr<Worker>& worker, const LilvState* state); + + boost::optional<Resource> save_preset(const URI& uri, + const Properties& props); + + void set_port_buffer(uint32_t voice, + uint32_t port_num, + BufferRef buf, + SampleCount offset); + + static LilvState* load_state(World* world, const FilePath& path); + +protected: + struct Instance : public Raul::Noncopyable { + explicit Instance(LilvInstance* i) : instance(i) {} + + ~Instance() { lilv_instance_free(instance); } + + LilvInstance* const instance; + }; + + SPtr<Instance> make_instance(URIs& uris, + SampleRate rate, + uint32_t voice, + bool preparing); + + inline LilvInstance* instance(uint32_t voice) { + return (LilvInstance*)(*_instances)[voice]->instance; + } + + typedef Raul::Array< SPtr<Instance> > Instances; + + void drop_instances(const MPtr<Instances>& instances) { + if (instances) { + for (size_t i = 0; i < instances->size(); ++i) { + (*instances)[i].reset(); + } + } + } + + struct Response : public Raul::Maid::Disposable + , public Raul::Noncopyable + , public boost::intrusive::slist_base_hook<> + { + inline Response(uint32_t s, const void* d) + : size(s) + , data(malloc(s)) + { + memcpy(data, d, s); + } + + ~Response() { + free(data); + } + + const uint32_t size; + void* const data; + }; + + typedef boost::intrusive::slist<Response, + boost::intrusive::cache_last<true>, + boost::intrusive::constant_time_size<false> + > Responses; + + static LV2_Worker_Status work_respond( + LV2_Worker_Respond_Handle handle, uint32_t size, const void* data); + + LV2Plugin* _lv2_plugin; + MPtr<Instances> _instances; + MPtr<Instances> _prepared_instances; + const LV2_Worker_Interface* _worker_iface; + std::mutex _work_mutex; + Responses _responses; + SPtr<LV2Features::FeatureArray> _features; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LV2BLOCK_HPP diff --git a/src/server/LV2Options.hpp b/src/server/LV2Options.hpp new file mode 100644 index 00000000..ef7c5ec9 --- /dev/null +++ b/src/server/LV2Options.hpp @@ -0,0 +1,71 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LV2OPTIONS_HPP +#define INGEN_ENGINE_LV2OPTIONS_HPP + +#include "ingen/LV2Features.hpp" +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/options/options.h" + +namespace Ingen { +namespace Server { + +class LV2Options : public Ingen::LV2Features::Feature { +public: + explicit LV2Options(const URIs& uris) + : _uris(uris) + {} + + void set(int32_t sample_rate, int32_t block_length, int32_t seq_size) { + _sample_rate = sample_rate; + _block_length = block_length; + _seq_size = seq_size; + } + + const char* uri() const { return LV2_OPTIONS__options; } + + SPtr<LV2_Feature> feature(World* w, Node* n) { + const LV2_Options_Option options[] = { + { LV2_OPTIONS_INSTANCE, 0, _uris.bufsz_minBlockLength, + sizeof(int32_t), _uris.atom_Int, &_block_length }, + { LV2_OPTIONS_INSTANCE, 0, _uris.bufsz_maxBlockLength, + sizeof(int32_t), _uris.atom_Int, &_block_length }, + { LV2_OPTIONS_INSTANCE, 0, _uris.bufsz_sequenceSize, + sizeof(int32_t), _uris.atom_Int, &_seq_size }, + { LV2_OPTIONS_INSTANCE, 0, _uris.param_sampleRate, + sizeof(int32_t), _uris.atom_Int, &_sample_rate }, + { LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, nullptr } + }; + + LV2_Feature* f = (LV2_Feature*)malloc(sizeof(LV2_Feature)); + f->URI = LV2_OPTIONS__options; + f->data = malloc(sizeof(options)); + memcpy(f->data, options, sizeof(options)); + return SPtr<LV2_Feature>(f, &free_feature); + } + +private: + const URIs& _uris; + int32_t _sample_rate; + int32_t _block_length; + int32_t _seq_size; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LV2OPTIONS_HPP diff --git a/src/server/LV2Plugin.cpp b/src/server/LV2Plugin.cpp new file mode 100644 index 00000000..f56fd4d7 --- /dev/null +++ b/src/server/LV2Plugin.cpp @@ -0,0 +1,143 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/Forge.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/presets/presets.h" + +#include "Engine.hpp" +#include "LV2Block.hpp" +#include "LV2Plugin.hpp" + +namespace Ingen { +namespace Server { + +LV2Plugin::LV2Plugin(World* world, const LilvPlugin* lplugin) + : PluginImpl(world->uris(), + world->uris().lv2_Plugin.urid, + URI(lilv_node_as_uri(lilv_plugin_get_uri(lplugin)))) + , _world(world) + , _lilv_plugin(lplugin) +{ + set_property(_uris.rdf_type, _uris.lv2_Plugin); + + update_properties(); +} + +void +LV2Plugin::update_properties() +{ + LilvNode* minor = lilv_world_get(_world->lilv_world(), + lilv_plugin_get_uri(_lilv_plugin), + _uris.lv2_minorVersion, + nullptr); + LilvNode* micro = lilv_world_get(_world->lilv_world(), + lilv_plugin_get_uri(_lilv_plugin), + _uris.lv2_microVersion, + nullptr); + + if (lilv_node_is_int(minor) && lilv_node_is_int(micro)) { + set_property(_uris.lv2_minorVersion, + _world->forge().make(lilv_node_as_int(minor))); + set_property(_uris.lv2_microVersion, + _world->forge().make(lilv_node_as_int(micro))); + } + + lilv_node_free(minor); + lilv_node_free(micro); +} + +const Raul::Symbol +LV2Plugin::symbol() const +{ + std::string working = uri(); + if (working.back() == '/') { + working = working.substr(0, working.length() - 1); + } + + while (working.length() > 0) { + size_t last_slash = working.find_last_of("/"); + const std::string symbol = working.substr(last_slash+1); + if ( (symbol[0] >= 'a' && symbol[0] <= 'z') + || (symbol[0] >= 'A' && symbol[0] <= 'Z') ) { + return Raul::Symbol::symbolify(symbol); + } else { + working = working.substr(0, last_slash); + } + } + + return Raul::Symbol("lv2_symbol"); +} + +BlockImpl* +LV2Plugin::instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state) +{ + LV2Block* b = new LV2Block( + this, symbol, polyphonic, parent, engine.sample_rate()); + + if (!b->instantiate(bufs, state)) { + delete b; + return nullptr; + } else { + return b; + } +} + +void +LV2Plugin::load_presets() +{ + const URIs& uris = _world->uris(); + LilvWorld* lworld = _world->lilv_world(); + LilvNodes* presets = lilv_plugin_get_related(_lilv_plugin, uris.pset_Preset); + + if (presets) { + LILV_FOREACH(nodes, i, presets) { + const LilvNode* preset = lilv_nodes_get(presets, i); + lilv_world_load_resource(lworld, preset); + + LilvNodes* labels = lilv_world_find_nodes( + lworld, preset, uris.rdfs_label, nullptr); + if (labels) { + const LilvNode* label = lilv_nodes_get_first(labels); + + _presets.emplace(URI(lilv_node_as_uri(preset)), + lilv_node_as_string(label)); + + lilv_nodes_free(labels); + } else { + _world->log().error( + fmt("Preset <%1%> has no rdfs:label\n") + % lilv_node_as_string(lilv_nodes_get(presets, i))); + } + } + + lilv_nodes_free(presets); + } + + PluginImpl::load_presets(); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/LV2Plugin.hpp b/src/server/LV2Plugin.hpp new file mode 100644 index 00000000..43d0fba9 --- /dev/null +++ b/src/server/LV2Plugin.hpp @@ -0,0 +1,72 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LV2PLUGIN_HPP +#define INGEN_ENGINE_LV2PLUGIN_HPP + +#include <cstdlib> + +#include "ingen/types.hpp" +#include "lilv/lilv.h" + +#include "PluginImpl.hpp" + +namespace Ingen { + +class World; + +namespace Server { + +class GraphImpl; +class BlockImpl; + +/** Implementation of an LV2 plugin (loaded shared library). + */ +class LV2Plugin : public PluginImpl +{ +public: + LV2Plugin(World* world, const LilvPlugin* lplugin); + + BlockImpl* instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state); + + const Raul::Symbol symbol() const; + + World* world() const { return _world; } + const LilvPlugin* lilv_plugin() const { return _lilv_plugin; } + + void update_properties(); + + void load_presets(); + + URI bundle_uri() const { + const LilvNode* bundle = lilv_plugin_get_bundle_uri(_lilv_plugin); + return URI(lilv_node_as_uri(bundle)); + } + +private: + World* _world; + const LilvPlugin* _lilv_plugin; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LV2PLUGIN_HPP diff --git a/src/server/LV2ResizeFeature.hpp b/src/server/LV2ResizeFeature.hpp new file mode 100644 index 00000000..f61165ee --- /dev/null +++ b/src/server/LV2ResizeFeature.hpp @@ -0,0 +1,65 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LV2RESIZEFEATURE_HPP +#define INGEN_ENGINE_LV2RESIZEFEATURE_HPP + +#include "ingen/LV2Features.hpp" +#include "lv2/lv2plug.in/ns/ext/resize-port/resize-port.h" + +#include "BlockImpl.hpp" +#include "Buffer.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +struct ResizeFeature : public Ingen::LV2Features::Feature { + static LV2_Resize_Port_Status resize_port( + LV2_Resize_Port_Feature_Data data, + uint32_t index, + size_t size) { + BlockImpl* block = (BlockImpl*)data; + PortImpl* port = block->port_impl(index); + if (block->context() == Context::ID::MESSAGE) { + port->buffer(0)->resize(size); + port->connect_buffers(); + return LV2_RESIZE_PORT_SUCCESS; + } + return LV2_RESIZE_PORT_ERR_UNKNOWN; + } + + const char* uri() const { return LV2_RESIZE_PORT_URI; } + + SPtr<LV2_Feature> feature(World* w, Node* n) { + BlockImpl* block = dynamic_cast<BlockImpl*>(n); + if (!block) + return SPtr<LV2_Feature>(); + LV2_Resize_Port_Resize* data + = (LV2_Resize_Port_Resize*)malloc(sizeof(LV2_Resize_Port_Resize)); + data->data = block; + data->resize = &resize_port; + LV2_Feature* f = (LV2_Feature*)malloc(sizeof(LV2_Feature)); + f->URI = LV2_RESIZE_PORT_URI; + f->data = data; + return SPtr<LV2_Feature>(f, &free_feature); + } +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LV2RESIZEFEATURE_HPP diff --git a/src/server/Load.hpp b/src/server/Load.hpp new file mode 100644 index 00000000..ed9ee406 --- /dev/null +++ b/src/server/Load.hpp @@ -0,0 +1,57 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_LOAD_HPP +#define INGEN_ENGINE_LOAD_HPP + +namespace Ingen { +namespace Server { + +struct Load +{ + void update(uint64_t time, uint64_t available) { + const uint64_t load = time * 100 / available; + if (load < min) { + min = load; + changed = true; + } + if (load > max) { + max = load; + changed = true; + } + if (++n == 1) { + mean = load; + changed = true; + } else { + const float a = mean + ((float)load - mean) / (float)++n; + if (a != mean) { + changed = floorf(a) != floorf(mean); + mean = a; + } + } + } + + uint64_t min = std::numeric_limits<uint64_t>::max(); + uint64_t max = 0; + float mean = 0.0f; + uint64_t n = 0; + bool changed = false; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_LOAD_HPP diff --git a/src/server/NodeImpl.cpp b/src/server/NodeImpl.cpp new file mode 100644 index 00000000..778ba15a --- /dev/null +++ b/src/server/NodeImpl.cpp @@ -0,0 +1,50 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "GraphImpl.hpp" +#include "NodeImpl.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +NodeImpl::NodeImpl(const Ingen::URIs& uris, + NodeImpl* parent, + const Raul::Symbol& symbol) + : Node(uris, parent ? parent->path().child(symbol) : Raul::Path("/")) + , _parent(parent) + , _path(parent ? parent->path().child(symbol) : Raul::Path("/")) + , _symbol(symbol) +{ +} + +const Atom& +NodeImpl::get_property(const URI& key) const +{ + ThreadManager::assert_not_thread(THREAD_PROCESS); + static const Atom null_atom; + auto i = properties().find(key); + return (i != properties().end()) ? i->second : null_atom; +} + +GraphImpl* +NodeImpl::parent_graph() const +{ + return dynamic_cast<GraphImpl*>((BlockImpl*)_parent); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/NodeImpl.hpp b/src/server/NodeImpl.hpp new file mode 100644 index 00000000..614801eb --- /dev/null +++ b/src/server/NodeImpl.hpp @@ -0,0 +1,109 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_NODEIMPL_HPP +#define INGEN_ENGINE_NODEIMPL_HPP + +#include <cassert> +#include <cstddef> +#include <map> + +#include "ingen/Node.hpp" +#include "ingen/Resource.hpp" +#include "raul/Deletable.hpp" +#include "raul/Path.hpp" + +namespace Raul { class Maid; } + +namespace Ingen { + +namespace Shared { class URIs; } + +namespace Server { + +class BufferFactory; +class GraphImpl; +class RunContext; + +/** An object on the audio graph (a Graph, Block, or Port). + * + * Each of these is a Raul::Deletable and so can be deleted in a realtime safe + * way from anywhere, and they all have a map of variable for clients to store + * arbitrary values in (which the engine puts no significance to whatsoever). + * + * \ingroup engine + */ +class NodeImpl : public Node +{ +public: + const Raul::Symbol& symbol() const { return _symbol; } + + Node* graph_parent() const { return _parent; } + NodeImpl* parent() const { return _parent; } + + /** Rename */ + virtual void set_path(const Raul::Path& new_path) { + _path = new_path; + const char* const new_sym = new_path.symbol(); + if (new_sym[0] != '\0') { + _symbol = Raul::Symbol(new_sym); + } + set_uri(path_to_uri(new_path)); + } + + const Atom& get_property(const URI& key) const; + + /** The Graph this object is a child of. */ + virtual GraphImpl* parent_graph() const; + + const Raul::Path& path() const { return _path; } + + /** Prepare for a new (external) polyphony value. + * + * Preprocessor thread, poly is actually applied by apply_poly. + * \return true on success. + */ + virtual bool prepare_poly(BufferFactory& bufs, uint32_t poly) = 0; + + /** Apply a new (external) polyphony value. + * + * \param context Process context (process thread only). + * \param poly Must be <= the most recent value passed to prepare_poly. + */ + virtual bool apply_poly(RunContext& context, uint32_t poly) = 0; + + /** Return true iff this is main (the top level Node). + * + * This is sometimes called "the root graph", but the term "main" is used + * to avoid ambiguity with the root path, since main does not have the path + * "/", but usually "/main" to leave namespace for non-node things. + */ + bool is_main() const { return !_parent; } + +protected: + NodeImpl(const Ingen::URIs& uris, + NodeImpl* parent, + const Raul::Symbol& symbol); + + NodeImpl* _parent; + Raul::Path _path; + Raul::Symbol _symbol; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_NODEIMPL_HPP diff --git a/src/server/OutputPort.hpp b/src/server/OutputPort.hpp new file mode 100644 index 00000000..1058defb --- /dev/null +++ b/src/server/OutputPort.hpp @@ -0,0 +1,51 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_OUTPUTPORT_HPP +#define INGEN_ENGINE_OUTPUTPORT_HPP + +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { + +/** An output port. + * + * Output ports always have a locally allocated buffer, and buffer() will + * always return that buffer. + * + * \ingroup engine + */ +class OutputPort : public PortImpl +{ +public: + OutputPort(BufferFactory& bufs, + BlockImpl* parent, + const Raul::Symbol& symbol, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size = 0) + : PortImpl(bufs, parent, symbol, index,poly, type, buffer_type, value, buffer_size, true) + {} +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_OUTPUTPORT_HPP diff --git a/src/server/PluginImpl.hpp b/src/server/PluginImpl.hpp new file mode 100644 index 00000000..ebd4b3e5 --- /dev/null +++ b/src/server/PluginImpl.hpp @@ -0,0 +1,96 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PLUGINIMPL_HPP +#define INGEN_ENGINE_PLUGINIMPL_HPP + +#include <cstdlib> + +#include "ingen/Resource.hpp" +#include "raul/Symbol.hpp" + +namespace Ingen { + +class URIs; + +namespace Server { + +class BlockImpl; +class BufferFactory; +class Engine; +class GraphImpl; + +/** Implementation of a plugin (internal code, or a loaded shared library). + * + * Conceptually, a Block is an instance of this. + */ +class PluginImpl : public Resource +{ +public: + PluginImpl(Ingen::URIs& uris, const Atom& type, const URI& uri) + : Resource(uris, uri) + , _type(type) + , _presets_loaded(false) + , _is_zombie(false) + { + } + + virtual BlockImpl* instantiate(BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + Engine& engine, + const LilvState* state) = 0; + + virtual const Raul::Symbol symbol() const = 0; + + const Atom& type() const { return _type; } + void set_type(const Atom& t) { _type = t; } + bool is_zombie() const { return _is_zombie; } + void set_is_zombie(bool t) { _is_zombie = t; } + + typedef std::pair<URI, std::string> Preset; + typedef std::map<URI, std::string> Presets; + + const Presets& presets(bool force_reload=false) { + if (!_presets_loaded || force_reload) { + load_presets(); + } + + return _presets; + } + + virtual void update_properties() {} + + virtual void load_presets() { _presets_loaded = true; } + + virtual URI bundle_uri() const { return URI("ingen:/"); } + +protected: + Atom _type; + Presets _presets; + bool _presets_loaded; + bool _is_zombie; + +private: + PluginImpl(const PluginImpl&) = delete; + PluginImpl& operator=(const PluginImpl&) = delete; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PLUGINIMPL_HPP diff --git a/src/server/PortAudioDriver.cpp b/src/server/PortAudioDriver.cpp new file mode 100644 index 00000000..f892c99f --- /dev/null +++ b/src/server/PortAudioDriver.cpp @@ -0,0 +1,297 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen_config.h" + +#include <cstdlib> +#include <string> + +#include <portaudio.h> + +#include "ingen/Configuration.hpp" +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "Buffer.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PortAudioDriver.hpp" +#include "PortImpl.hpp" +#include "FrameTimer.hpp" +#include "ThreadManager.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { + +static bool +pa_error(const char* msg, PaError err) +{ + fprintf(stderr, "error: %s (%s)\n", msg, Pa_GetErrorText(err)); + Pa_Terminate(); + return false; +} + +PortAudioDriver::PortAudioDriver(Engine& engine) + : _engine(engine) + , _sem(0) + , _stream(nullptr) + , _seq_size(4096) + , _block_length(engine.world()->conf().option("buffer-size").get<int32_t>()) + , _sample_rate(48000) + , _n_inputs(0) + , _n_outputs(0) + , _flag(false) + , _is_activated(false) +{ +} + +PortAudioDriver::~PortAudioDriver() +{ + deactivate(); + _ports.clear_and_dispose([](EnginePort* p) { delete p; }); +} + +bool +PortAudioDriver::attach() +{ + PaError st = paNoError; + if ((st = Pa_Initialize())) { + return pa_error("Failed to initialize audio system", st); + } + + // Get default input and output devices + _inputParameters.device = Pa_GetDefaultInputDevice(); + _outputParameters.device = Pa_GetDefaultOutputDevice(); + if (_inputParameters.device == paNoDevice) { + return pa_error("No default input device", paDeviceUnavailable); + } else if (_outputParameters.device == paNoDevice) { + return pa_error("No default output device", paDeviceUnavailable); + } + + const PaDeviceInfo* in_dev = Pa_GetDeviceInfo(_inputParameters.device); + + /* TODO: It looks like it is somehow actually impossible to request the + best/native buffer size then retrieve what it actually is with + PortAudio. How such a glaring useless flaw exists in such a widespread + library is beyond me... */ + + _sample_rate = in_dev->defaultSampleRate; + + _timer = std::unique_ptr<FrameTimer>( + new FrameTimer(_block_length, _sample_rate)); + + return true; +} + +bool +PortAudioDriver::activate() +{ + const PaDeviceInfo* in_dev = Pa_GetDeviceInfo(_inputParameters.device); + const PaDeviceInfo* out_dev = Pa_GetDeviceInfo(_outputParameters.device); + + // Count number of input and output audio ports/channels + _inputParameters.channelCount = 0; + _outputParameters.channelCount = 0; + for (const auto& port : _ports) { + if (port.graph_port()->is_a(PortType::AUDIO)) { + if (port.graph_port()->is_input()) { + ++_inputParameters.channelCount; + } else if (port.graph_port()->is_output()) { + ++_outputParameters.channelCount; + } + } + } + + // Configure audio format + _inputParameters.sampleFormat = paFloat32|paNonInterleaved; + _inputParameters.suggestedLatency = in_dev->defaultLowInputLatency; + _inputParameters.hostApiSpecificStreamInfo = nullptr; + _outputParameters.sampleFormat = paFloat32|paNonInterleaved; + _outputParameters.suggestedLatency = out_dev->defaultLowOutputLatency; + _outputParameters.hostApiSpecificStreamInfo = nullptr; + + // Open stream + PaError st = paNoError; + if ((st = Pa_OpenStream( + &_stream, + _inputParameters.channelCount ? &_inputParameters : nullptr, + _outputParameters.channelCount ? &_outputParameters : nullptr, + in_dev->defaultSampleRate, + _block_length, // paFramesPerBufferUnspecified, // FIXME: ? + 0, + pa_process_cb, + this))) { + return pa_error("Failed to open audio stream", st); + } + + _is_activated = true; + if ((st = Pa_StartStream(_stream))) { + return pa_error("Error starting audio stream", st); + } + + return true; +} + +void +PortAudioDriver::deactivate() +{ + Pa_Terminate(); +} + +SampleCount +PortAudioDriver::frame_time() const +{ + return _timer->frame_time(_engine.current_time()) + _engine.block_length(); +} + +EnginePort* +PortAudioDriver::get_port(const Raul::Path& path) +{ + for (auto& p : _ports) { + if (p.graph_port()->path() == path) { + return &p; + } + } + + return nullptr; +} + +void +PortAudioDriver::add_port(RunContext& context, EnginePort* port) +{ + _ports.push_back(*port); +} + +void +PortAudioDriver::remove_port(RunContext& context, EnginePort* port) +{ + _ports.erase(_ports.iterator_to(*port)); +} + +void +PortAudioDriver::register_port(EnginePort& port) +{ +} + +void +PortAudioDriver::unregister_port(EnginePort& port) +{ +} + +void +PortAudioDriver::rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) +{ +} + +void +PortAudioDriver::port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) +{ +} + +EnginePort* +PortAudioDriver::create_port(DuplexPort* graph_port) +{ + EnginePort* eport = nullptr; + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + // Audio buffer port, use Jack buffer directly + eport = new EnginePort(graph_port); + graph_port->set_is_driver_port(*_engine.buffer_factory()); + } else if (graph_port->is_a(PortType::ATOM) && + graph_port->buffer_type() == _engine.world()->uris().atom_Sequence) { + // Sequence port, make Jack port but use internal LV2 format buffer + eport = new EnginePort(graph_port); + } + + if (graph_port->is_a(PortType::AUDIO)) { + if (graph_port->is_input()) { + eport->set_driver_index(_n_inputs++); + } else { + eport->set_driver_index(_n_outputs++); + } + } + + if (eport) { + register_port(*eport); + } + + return eport; +} + +void +PortAudioDriver::pre_process_port(RunContext& context, + EnginePort* port, + const void* inputs, + void* outputs) +{ + if (!port->graph_port()->is_a(PortType::AUDIO)) { + return; + } + + if (port->is_input()) { + port->set_buffer(((float**)inputs)[port->driver_index()]); + } else { + port->set_buffer(((float**)outputs)[port->driver_index()]); + memset(port->buffer(), 0, _block_length * sizeof(float)); + } + + port->graph_port()->set_driver_buffer( + port->buffer(), _block_length * sizeof(float)); +} + +void +PortAudioDriver::post_process_port(RunContext& context, + EnginePort* port, + const void* inputs, + void* outputs) +{ +} + +int +PortAudioDriver::process_cb(const void* inputs, + void* outputs, + unsigned long nframes, + const PaStreamCallbackTimeInfo* time, + PaStreamCallbackFlags flags) +{ + _engine.advance(nframes); + _timer->update(_engine.current_time(), _engine.run_context().start()); + + // Read input + for (auto& p : _ports) { + pre_process_port(_engine.run_context(), &p, inputs, outputs); + } + + // Process + _engine.run(nframes); + + // Write output + for (auto& p : _ports) { + post_process_port(_engine.run_context(), &p, inputs, outputs); + } + + return 0; +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/PortAudioDriver.hpp b/src/server/PortAudioDriver.hpp new file mode 100644 index 00000000..b1545f64 --- /dev/null +++ b/src/server/PortAudioDriver.hpp @@ -0,0 +1,132 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PORTAUDIODRIVER_HPP +#define INGEN_ENGINE_PORTAUDIODRIVER_HPP + +#include "ingen_config.h" + +#include <atomic> +#include <memory> +#include <string> + +#include <portaudio.h> + +#include "raul/Semaphore.hpp" + +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" + +#include "Driver.hpp" +#include "EnginePort.hpp" + +namespace Raul { class Path; } + +namespace Ingen { +namespace Server { + +class DuplexPort; +class Engine; +class GraphImpl; +class PortAudioDriver; +class PortImpl; +class FrameTimer; + +class PortAudioDriver : public Driver +{ +public: + explicit PortAudioDriver(Engine& engine); + ~PortAudioDriver(); + + bool attach(); + + bool activate(); + void deactivate(); + + EnginePort* create_port(DuplexPort* graph_port); + EnginePort* get_port(const Raul::Path& path); + + void rename_port(const Raul::Path& old_path, const Raul::Path& new_path); + void port_property(const Raul::Path& path, const URI& uri, const Atom& value); + void add_port(RunContext& context, EnginePort* port); + void remove_port(RunContext& context, EnginePort* port); + void register_port(EnginePort& port); + void unregister_port(EnginePort& port); + + void append_time_events(RunContext& context, Buffer& buffer) {} + + SampleCount frame_time() const; + + int real_time_priority() { return 80; } + + SampleCount block_length() const { return _block_length; } + size_t seq_size() const { return _seq_size; } + SampleCount sample_rate() const { return _sample_rate; } + +private: + friend class PortAudioPort; + + inline static int + pa_process_cb(const void* inputs, + void* outputs, + unsigned long nframes, + const PaStreamCallbackTimeInfo* time, + PaStreamCallbackFlags flags, + void* handle) { + return ((PortAudioDriver*)handle)->process_cb( + inputs, outputs, nframes, time, flags); + } + + int process_cb(const void* inputs, + void* outputs, + unsigned long nframes, + const PaStreamCallbackTimeInfo* time, + PaStreamCallbackFlags flags); + + void pre_process_port(RunContext& context, + EnginePort* port, + const void* inputs, + void* outputs); + + void post_process_port(RunContext& context, + EnginePort* port, + const void* inputs, + void* outputs); + +protected: + typedef boost::intrusive::slist<EnginePort, + boost::intrusive::cache_last<true> + > Ports; + + Engine& _engine; + Ports _ports; + PaStreamParameters _inputParameters; + PaStreamParameters _outputParameters; + Raul::Semaphore _sem; + std::unique_ptr<FrameTimer> _timer; + PaStream* _stream; + size_t _seq_size; + uint32_t _block_length; + uint32_t _sample_rate; + uint32_t _n_inputs; + uint32_t _n_outputs; + std::atomic<bool> _flag; + bool _is_activated; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PORTAUDIODRIVER_HPP diff --git a/src/server/PortImpl.cpp b/src/server/PortImpl.cpp new file mode 100644 index 00000000..b0ef3c85 --- /dev/null +++ b/src/server/PortImpl.cpp @@ -0,0 +1,569 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "raul/Array.hpp" +#include "raul/Maid.hpp" + +#include "BlockImpl.hpp" +#include "Buffer.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "PortImpl.hpp" +#include "PortType.hpp" +#include "ThreadManager.hpp" + +namespace Ingen { +namespace Server { + +static const uint32_t monitor_rate = 25.0; // Hz + +/** The length of time between monitor updates in frames */ +static inline uint32_t +monitor_period(const Engine& engine) +{ + return std::max(engine.block_length(), + engine.sample_rate() / monitor_rate); +} + +PortImpl::PortImpl(BufferFactory& bufs, + BlockImpl* const block, + const Raul::Symbol& name, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size, + bool is_output) + : NodeImpl(bufs.uris(), block, name) + , _bufs(bufs) + , _index(index) + , _poly(poly) + , _buffer_size(buffer_size) + , _frames_since_monitor(0) + , _monitor_value(0.0f) + , _peak(0.0f) + , _type(type) + , _buffer_type(buffer_type) + , _value(value) + , _min(bufs.forge().make(0.0f)) + , _max(bufs.forge().make(1.0f)) + , _voices(bufs.maid().make_managed<Voices>(poly)) + , _connected_flag(false) + , _monitored(false) + , _force_monitor_update(false) + , _is_morph(false) + , _is_auto_morph(false) + , _is_logarithmic(false) + , _is_sample_rate(false) + , _is_toggled(false) + , _is_driver_port(false) + , _is_output(is_output) +{ + assert(block != nullptr); + assert(_poly > 0); + + const Ingen::URIs& uris = bufs.uris(); + + set_type(type, buffer_type); + + remove_property(uris.lv2_index, uris.patch_wildcard); + set_property(uris.lv2_index, bufs.forge().make((int32_t)index)); + + if (has_value()) { + set_property(uris.ingen_value, value); + } + if (type == PortType::ATOM) { + set_property(uris.atom_bufferType, + bufs.forge().make_urid(buffer_type)); + } + + if (is_output) { + if (_parent->graph_type() != Node::GraphType::GRAPH) { + add_property(bufs.uris().rdf_type, bufs.uris().lv2_OutputPort.urid); + } + } + + get_buffers(bufs, &BufferFactory::get_buffer, _voices, poly, 0); +} + +bool +PortImpl::get_buffers(BufferFactory& bufs, + GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const +{ + for (uint32_t v = 0; v < poly; ++v) { + voices->at(v).buffer.reset(); + voices->at(v).buffer = (bufs.*get)( + buffer_type(), _value.type(), _buffer_size); + } + + return true; +} + +bool +PortImpl::setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly) +{ + return get_buffers(bufs, &BufferFactory::claim_buffer, _voices, poly, 0); +} + +void +PortImpl::set_type(PortType port_type, LV2_URID buffer_type) +{ + const Ingen::URIs& uris = _bufs.uris(); + Ingen::World* world = _bufs.engine().world(); + + // Update type properties so clients are aware of current type + remove_property(uris.rdf_type, uris.lv2_AudioPort); + remove_property(uris.rdf_type, uris.lv2_CVPort); + remove_property(uris.rdf_type, uris.lv2_ControlPort); + remove_property(uris.rdf_type, uris.atom_AtomPort); + add_property(uris.rdf_type, world->forge().make_urid(port_type.uri())); + + // Update audio thread types + _type = port_type; + _buffer_type = buffer_type; + if (!_buffer_type) { + switch (_type.id()) { + case PortType::CONTROL: + _buffer_type = uris.atom_Float; + break; + case PortType::AUDIO: + case PortType::CV: + _buffer_type = uris.atom_Sound; + break; + default: + break; + } + } + _buffer_size = std::max(_buffer_size, _bufs.default_size(_buffer_type)); +} + +bool +PortImpl::has_value() const +{ + return (_type == PortType::CONTROL || + _type == PortType::CV || + (_type == PortType::ATOM && + _value.type() == _bufs.uris().atom_Float)); +} + +bool +PortImpl::supports(const URIs::Quark& value_type) const +{ + return has_property(_bufs.uris().atom_supports, value_type); +} + +void +PortImpl::activate(BufferFactory& bufs) +{ + /* Set the time since the last monitor update to a random value within the + monitor period, to spread the load out over time. Otherwise, every + port would try to send an update at exactly the same time, every time. + */ + const double srate = bufs.engine().sample_rate(); + const uint32_t period = srate / monitor_rate; + _frames_since_monitor = bufs.engine().frand() * period; + _monitor_value = 0.0f; + _peak = 0.0f; + + // Trigger buffer re-connect next cycle + _connected_flag.clear(std::memory_order_release); +} + +void +PortImpl::deactivate() +{ + if (is_output() && !_is_driver_port) { + for (uint32_t v = 0; v < _poly; ++v) { + if (_voices->at(v).buffer) { + _voices->at(v).buffer->clear(); + } + } + } + _monitor_value = 0.0f; + _peak = 0.0f; +} + +void +PortImpl::set_voices(RunContext& context, MPtr<Voices>&& voices) +{ + _voices = std::move(voices); + connect_buffers(); +} + +void +PortImpl::cache_properties() +{ + _is_logarithmic = has_property(_bufs.uris().lv2_portProperty, + _bufs.uris().pprops_logarithmic); + _is_sample_rate = has_property(_bufs.uris().lv2_portProperty, + _bufs.uris().lv2_sampleRate); + _is_toggled = has_property(_bufs.uris().lv2_portProperty, + _bufs.uris().lv2_toggled); +} + +void +PortImpl::set_control_value(const RunContext& context, + FrameTime time, + Sample value) +{ + for (uint32_t v = 0; v < _poly; ++v) { + update_set_state(context, v); + set_voice_value(context, v, time, value); + } +} + +void +PortImpl::set_voice_value(const RunContext& context, + uint32_t voice, + FrameTime time, + Sample value) +{ + switch (_type.id()) { + case PortType::CONTROL: + if (buffer(voice)->value()) { + ((LV2_Atom_Float*)buffer(voice)->value())->body = value; + } + _voices->at(voice).set_state.set(context, context.start(), value); + break; + case PortType::AUDIO: + case PortType::CV: { + // Time may be at end so internal blocks can set triggers + assert(time >= context.start()); + assert(time <= context.start() + context.nframes()); + + const FrameTime offset = time - context.start(); + if (offset < context.nframes()) { + buffer(voice)->set_block(value, offset, context.nframes()); + } + /* else, this is a set at context.nframes(), used to reset a CV port's + value for the next block, particularly for triggers on the last + frame of a block (set nframes-1 to 1, then nframes to 0). */ + + _voices->at(voice).set_state.set(context, time, value); + } break; + case PortType::ATOM: + if (buffer(voice)->is_sequence()) { + const FrameTime offset = time - context.start(); + // Same deal as above + if (offset < context.nframes()) { + buffer(voice)->append_event(offset, + sizeof(value), + _bufs.uris().atom_Float, + (const uint8_t*)&value); + } + _voices->at(voice).set_state.set(context, time, value); + } else { +#ifndef NDEBUG + fprintf(stderr, + "error: %s set non-sequence atom port value (buffer type %u)\n", + path().c_str(), buffer(voice)->type()); +#endif + } + default: + break; + } +} + +void +PortImpl::update_set_state(const RunContext& context, uint32_t v) +{ + Voice& voice = _voices->at(v); + SetState& state = voice.set_state; + BufferRef buf = voice.buffer; + switch (state.state) { + case SetState::State::SET: + break; + case SetState::State::SET_CYCLE_1: + if (state.time < context.start() && + buf->is_sequence() && + buf->value_type() == _bufs.uris().atom_Float && + !_parent->is_main()) { + buf->clear(); + state.time = context.start(); + } + state.state = SetState::State::SET; + break; + case SetState::State::HALF_SET_CYCLE_1: + state.state = SetState::State::HALF_SET_CYCLE_2; + break; + case SetState::State::HALF_SET_CYCLE_2: + if (buf->is_sequence()) { + buf->clear(); + buf->append_event( + 0, sizeof(float), _bufs.uris().atom_Float, + (const uint8_t*)&state.value); + } else { + buf->set_block(state.value, 0, context.nframes()); + } + state.state = SetState::State::SET_CYCLE_1; + break; + } +} + +bool +PortImpl::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + if (_is_driver_port || _parent->is_main() || + (_type == PortType::ATOM && !_value.is_valid())) { + return false; + } else if (_poly == poly) { + return true; + } else if (_prepared_voices && _prepared_voices->size() != poly) { + _prepared_voices.reset(); + } + + if (!_prepared_voices) { + _prepared_voices = bufs.maid().make_managed<Voices>( + poly, *_voices, Voice()); + } + + get_buffers(bufs, &BufferFactory::get_buffer, + _prepared_voices, _prepared_voices->size(), num_arcs()); + + return true; +} + +bool +PortImpl::apply_poly(RunContext& context, uint32_t poly) +{ + if (_parent->is_main() || + (_type == PortType::ATOM && !_value.is_valid())) { + return false; + } else if (!_prepared_voices) { + return true; + } + + assert(poly == _prepared_voices->size()); + + _poly = poly; + + // Apply a new set of voices from a preceding call to prepare_poly + _voices = std::move(_prepared_voices); + + if (is_a(PortType::CONTROL) || is_a(PortType::CV)) { + set_control_value(context, context.start(), _value.get<float>()); + } + + assert(_voices->size() >= poly); + assert(this->poly() == poly); + assert(!_prepared_voices); + + connect_buffers(); + + return true; +} + +void +PortImpl::set_buffer_size(RunContext& context, BufferFactory& bufs, size_t size) +{ + _buffer_size = size; + + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer->resize(size); + } + + connect_buffers(); +} + +void +PortImpl::connect_buffers(SampleCount offset) +{ + for (uint32_t v = 0; v < _poly; ++v) { + PortImpl::parent_block()->set_port_buffer(v, _index, buffer(v), offset); + } +} + +void +PortImpl::recycle_buffers() +{ + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer = nullptr; + } +} + +void +PortImpl::set_is_driver_port(BufferFactory& bufs) +{ + _is_driver_port = true; +} + +void +PortImpl::clear_buffers(const RunContext& ctx) +{ + switch (_type.id()) { + case PortType::AUDIO: + default: + for (uint32_t v = 0; v < _poly; ++v) { + buffer(v)->clear(); + } + break; + case PortType::CONTROL: + for (uint32_t v = 0; v < _poly; ++v) { + buffer(v)->clear(); + _voices->at(v).set_state.set(ctx, ctx.start(), _value.get<float>()); + } + break; + case PortType::CV: + for (uint32_t v = 0; v < _poly; ++v) { + buffer(v)->set_block(_value.get<float>(), 0, ctx.nframes()); + _voices->at(v).set_state.set(ctx, ctx.start(), _value.get<float>()); + } + break; + } +} + +void +PortImpl::monitor(RunContext& context, bool send_now) +{ + if (!context.must_notify(this)) { + return; + } + + const uint32_t period = monitor_period(context.engine()); + _frames_since_monitor += context.nframes(); + + const bool time_to_send = send_now || _frames_since_monitor >= period; + const bool is_sequence = (_type.id() == PortType::ATOM && + _buffer_type == _bufs.uris().atom_Sequence); + if (!time_to_send && !(is_sequence && _monitored) && (!is_sequence && buffer(0)->value())) { + return; + } + + Forge& forge = context.engine().world()->forge(); + URIs& uris = context.engine().world()->uris(); + LV2_URID key = 0; + float val = 0.0f; + switch (_type.id()) { + case PortType::UNKNOWN: + break; + case PortType::AUDIO: + key = uris.ingen_activity; + val = _peak = std::max(_peak, buffer(0)->peak(context)); + break; + case PortType::CONTROL: + case PortType::CV: + key = uris.ingen_value; + val = buffer(0)->value_at(0); + break; + case PortType::ATOM: + if (_buffer_type == _bufs.uris().atom_Sequence) { + const LV2_Atom* atom = buffer(0)->get<const LV2_Atom>(); + const LV2_Atom* value = buffer(0)->value(); + if (atom->type != _bufs.uris().atom_Sequence) { + /* Buffer contents are not actually a Sequence. Probably an + uninitialized Chunk, so do nothing. */ + } else if (_monitored) { + /* Sequence explicitly monitored, send everything. */ + const LV2_Atom_Sequence* seq = (const LV2_Atom_Sequence*)atom; + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + context.notify(uris.ingen_activity, + context.start() + ev->time.frames, + this, + ev->body.size, + ev->body.type, + LV2_ATOM_BODY(&ev->body)); + } + } else if (value && value->type == _bufs.uris().atom_Float) { + /* Float sequence, monitor as a control. */ + key = uris.ingen_value; + val = ((LV2_Atom_Float*)buffer(0)->value())->body; + } else if (atom->size > sizeof(LV2_Atom_Sequence_Body)) { + /* General sequence, send activity for blinkenlights. */ + const int32_t one = 1; + context.notify(uris.ingen_activity, + context.start(), + this, + sizeof(int32_t), + (LV2_URID)uris.atom_Bool, + &one); + _force_monitor_update = false; + } + } + } + + _frames_since_monitor = _frames_since_monitor % period; + if (key && val != _monitor_value) { + if (context.notify(key, context.start(), this, + sizeof(float), forge.Float, &val)) { + /* Update frames since last update to conceptually zero, but keep + the remainder to preserve load balancing. */ + _frames_since_monitor = _frames_since_monitor % period; + _peak = 0.0f; + _monitor_value = val; + } + // Otherwise failure, leave old value and try again next time + } +} + +BufferRef +PortImpl::value_buffer(uint32_t voice) +{ + return buffer(voice)->value_buffer(); +} + +SampleCount +PortImpl::next_value_offset(SampleCount offset, SampleCount end) const +{ + SampleCount earliest = end; + for (uint32_t v = 0; v < _poly; ++v) { + const SampleCount o = _voices->at(v).buffer->next_value_offset(offset, end); + if (o < earliest) { + earliest = o; + } + } + return earliest; +} + +void +PortImpl::update_values(SampleCount offset, uint32_t voice) +{ + buffer(voice)->update_value_buffer(offset); +} + +void +PortImpl::pre_process(RunContext& context) +{ + if (!_connected_flag.test_and_set(std::memory_order_acquire)) { + connect_buffers(); + clear_buffers(context); + } + + for (uint32_t v = 0; v < _poly; ++v) { + _voices->at(v).buffer->prepare_output_write(context); + } +} + +void +PortImpl::post_process(RunContext& context) +{ + for (uint32_t v = 0; v < _poly; ++v) { + update_set_state(context, v); + update_values(0, v); + } + + monitor(context); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/PortImpl.hpp b/src/server/PortImpl.hpp new file mode 100644 index 00000000..70d90d0a --- /dev/null +++ b/src/server/PortImpl.hpp @@ -0,0 +1,312 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PORTIMPL_HPP +#define INGEN_ENGINE_PORTIMPL_HPP + +#include <cstdlib> + +#include "ingen/Atom.hpp" +#include "raul/Array.hpp" + +#include "BufferRef.hpp" +#include "NodeImpl.hpp" +#include "PortType.hpp" +#include "RunContext.hpp" +#include "types.hpp" + +namespace Raul { class Maid; } + +namespace Ingen { +namespace Server { + +class BlockImpl; +class BufferFactory; +class RunContext; + +/** A port (input or output) on a Block. + * + * The base implementation here is general and/or for output ports (which are + * simplest), InputPort and DuplexPort override functions to provide + * specialized behaviour where necessary. + * + * \ingroup engine + */ +class PortImpl : public NodeImpl +{ +public: + struct SetState { + enum class State { + /// Partially set, first cycle: AAAAA => AAABB. + HALF_SET_CYCLE_1, + + /// Partially set, second cycle: AAABB => BBBBB. + HALF_SET_CYCLE_2, + + /// Fully set, first cycle (clear events if necessary). + SET_CYCLE_1, + + /// Fully set, second cycle and onwards (done). + SET + }; + + SetState() : state(State::SET), value(0), time(0) {} + + void set(const RunContext& context, FrameTime t, Sample v) { + time = t; + value = v; + state = (time == context.start() + ? State::SET + : State::HALF_SET_CYCLE_1); + } + + State state; ///< State of buffer for setting control value + Sample value; ///< Value currently being set + FrameTime time; ///< Time value was set + }; + + struct Voice { + Voice() : buffer(nullptr) {} + + SetState set_state; + BufferRef buffer; + }; + + typedef Raul::Array<Voice> Voices; + + PortImpl(BufferFactory& bufs, + BlockImpl* block, + const Raul::Symbol& name, + uint32_t index, + uint32_t poly, + PortType type, + LV2_URID buffer_type, + const Atom& value, + size_t buffer_size = 0, + bool is_output = true); + + virtual GraphType graph_type() const { return GraphType::PORT; } + + /** A port's parent is always a block, so static cast should be safe */ + BlockImpl* parent_block() const { return (BlockImpl*)_parent; } + + /** Set the the voices (buffers) for this port in the audio thread. */ + void set_voices(RunContext& context, MPtr<Voices>&& voices); + + /** Prepare for a new (external) polyphony value. + * + * Preprocessor thread, poly is actually applied by apply_poly. + */ + virtual bool prepare_poly(BufferFactory& bufs, uint32_t poly); + + /** Apply a new polyphony value. + * + * Audio thread. + * \a poly Must be < the most recent value passed to prepare_poly. + */ + virtual bool apply_poly(RunContext& context, uint32_t poly); + + /** Return the number of arcs (pre-process thraed). */ + virtual size_t num_arcs() const { return 0; } + + const Atom& value() const { return _value; } + void set_value(const Atom& v) { _value = v; } + + const Atom& minimum() const { return _min; } + const Atom& maximum() const { return _max; } + + /* The following two methods store the range in variables so it can be + accessed in the process thread, which is required for applying control + bindings from incoming MIDI data. + */ + void set_minimum(const Atom& min) { _min.set_rt(min); } + void set_maximum(const Atom& max) { _max.set_rt(max); } + + inline BufferRef buffer(uint32_t voice) const { + return _voices->at((_poly == 1) ? 0 : voice).buffer; + } + inline BufferRef prepared_buffer(uint32_t voice) const { + return _prepared_voices->at(voice).buffer; + } + + void update_set_state(const RunContext& context, uint32_t v); + + void set_voice_value(const RunContext& context, + uint32_t voice, + FrameTime time, + Sample value); + + void set_control_value(const RunContext& context, + FrameTime time, + Sample value); + + /** Prepare this port to use an external driver-provided buffer. + * + * This will avoid allocating a buffer for the port, instead the driver + * buffer is used directly. This only makes sense for ports on the + * top-level graph, which are monophonic. Non-real-time, must be called + * before using the port, followed by a call to set_driver_buffer() in the + * processing thread. + */ + virtual void set_is_driver_port(BufferFactory& bufs); + + bool is_driver_port() const { return _is_driver_port; } + + /** Called once per process cycle */ + virtual void pre_process(RunContext& context); + virtual void pre_run(RunContext& context) {} + virtual void post_process(RunContext& context); + + /** Clear/silence all buffers */ + virtual void clear_buffers(const RunContext& ctx); + + /** Claim and apply buffers in the real-time thread. */ + virtual bool setup_buffers(RunContext& ctx, BufferFactory& bufs, uint32_t poly); + + void activate(BufferFactory& bufs); + void deactivate(); + + /** + Inherit any properties from a connected neighbour. + + This is used for Graph ports, so e.g. a control input has the range of + all the ports it is connected to. + */ + virtual void inherit_neighbour(const PortImpl* port, + Properties& remove, + Properties& add) {} + + virtual void connect_buffers(SampleCount offset=0); + virtual void recycle_buffers(); + + uint32_t index() const { return _index; } + void set_index(RunContext&, uint32_t index) { _index = index; } + + inline bool is_a(PortType type) const { return _type == type; } + + bool has_value() const; + + PortType type() const { return _type; } + LV2_URID value_type() const { return _value.is_valid() ? _value.type() : 0; } + LV2_URID buffer_type() const { return _buffer_type; } + + bool supports(const URIs::Quark& value_type) const; + + size_t buffer_size() const { return _buffer_size; } + + uint32_t poly() const { + return _poly; + } + uint32_t prepared_poly() const { + return (_prepared_voices) ? _prepared_voices->size() : 1; + } + + void set_buffer_size(RunContext& context, BufferFactory& bufs, size_t size); + + /** Return true iff this port is explicitly monitored. + * + * This is used for plugin UIs which require monitoring for particular + * ports, even if the Ingen client has not requested broadcasting in + * general (e.g. for canvas animation). + */ + bool is_monitored() const { return _monitored; } + + /** Explicitly turn on monitoring for this port. */ + void enable_monitoring(bool monitored) { _monitored = monitored; } + + /** Monitor port value and broadcast to clients periodically. */ + void monitor(RunContext& context, bool send_now=false); + + BufferFactory& bufs() const { return _bufs; } + + BufferRef value_buffer(uint32_t voice); + + BufferRef user_buffer(RunContext&) const { return _user_buffer; } + void set_user_buffer(RunContext&, BufferRef b) { _user_buffer = b; } + + /** Return offset of the first value change after `offset`. */ + virtual SampleCount next_value_offset(SampleCount offset, + SampleCount end) const; + + /** Update value buffer for `voice` to be current as of `offset`. */ + void update_values(SampleCount offset, uint32_t voice); + + void force_monitor_update() { _force_monitor_update = true; } + + void set_morphable(bool is_morph, bool is_auto_morph) { + _is_morph = is_morph; + _is_auto_morph = is_auto_morph; + } + + void set_type(PortType port_type, LV2_URID buffer_type); + + void cache_properties(); + + bool is_input() const { return !_is_output; } + bool is_output() const { return _is_output; } + bool is_morph() const { return _is_morph; } + bool is_auto_morph() const { return _is_auto_morph; } + bool is_logarithmic() const { return _is_logarithmic; } + bool is_sample_rate() const { return _is_sample_rate; } + bool is_toggled() const { return _is_toggled; } + +protected: + typedef BufferRef (BufferFactory::*GetFn)(LV2_URID, LV2_URID, uint32_t); + + /** Set `voices` as the buffers to be used for this port. + * + * This is real-time safe only if `get` is as well, use in the real-time + * thread should pass &BufferFactory::claim_buffer. + * + * @return true iff buffers are locally owned by the port + */ + virtual bool get_buffers(BufferFactory& bufs, + GetFn get, + const MPtr<Voices>& voices, + uint32_t poly, + size_t num_in_arcs) const; + + BufferFactory& _bufs; + uint32_t _index; + uint32_t _poly; + uint32_t _buffer_size; + uint32_t _frames_since_monitor; + float _monitor_value; + float _peak; + PortType _type; + LV2_URID _buffer_type; + Atom _value; + Atom _min; + Atom _max; + MPtr<Voices> _voices; + MPtr<Voices> _prepared_voices; + BufferRef _user_buffer; + std::atomic_flag _connected_flag; + bool _monitored; + bool _force_monitor_update; + bool _is_morph; + bool _is_auto_morph; + bool _is_logarithmic; + bool _is_sample_rate; + bool _is_toggled; + bool _is_driver_port; + bool _is_output; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PORTIMPL_HPP diff --git a/src/server/PortType.hpp b/src/server/PortType.hpp new file mode 100644 index 00000000..0b62c5ab --- /dev/null +++ b/src/server/PortType.hpp @@ -0,0 +1,91 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERFACE_PORTTYPE_HPP +#define INGEN_INTERFACE_PORTTYPE_HPP + +#include <cassert> + +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +namespace Ingen { + +/** The type of a port. + * + * This type refers to the type of the port itself (not necessarily the type + * of its contents). Ports with different types can contain the same type of + * data, but may e.g. have different access semantics. + */ +class PortType { +public: + enum ID { + UNKNOWN = 0, + AUDIO = 1, + CONTROL = 2, + CV = 3, + ATOM = 4 + }; + + explicit PortType(const URI& uri) + : _id(UNKNOWN) + { + if (uri == type_uri(AUDIO)) { + _id = AUDIO; + } else if (uri == type_uri(CONTROL)) { + _id = CONTROL; + } else if (uri == type_uri(CV)) { + _id = CV; + } else if (uri == type_uri(ATOM)) { + _id = ATOM; + } + } + + PortType(ID id) : _id(id) {} + + inline const URI& uri() const { return type_uri(_id); } + inline ID id() const { return _id; } + + inline bool operator==(const ID& id) const { return (_id == id); } + inline bool operator!=(const ID& id) const { return (_id != id); } + inline bool operator==(const PortType& type) const { return (_id == type._id); } + inline bool operator!=(const PortType& type) const { return (_id != type._id); } + inline bool operator<(const PortType& type) const { return (_id < type._id); } + + inline bool is_audio() { return _id == AUDIO; } + inline bool is_control() { return _id == CONTROL; } + inline bool is_cv() { return _id == CV; } + inline bool is_atom() { return _id == ATOM; } + +private: + static inline const URI& type_uri(unsigned id_num) { + assert(id_num <= ATOM); + static const URI uris[] = { + URI("http://www.w3.org/2002/07/owl#Nothing"), + URI(LV2_CORE__AudioPort), + URI(LV2_CORE__ControlPort), + URI(LV2_CORE__CVPort), + URI(LV2_ATOM__AtomPort) + }; + return uris[id_num]; + } + + ID _id; +}; + +} // namespace Ingen + +#endif // INGEN_INTERFACE_PORTTYPE_HPP diff --git a/src/server/PostProcessor.cpp b/src/server/PostProcessor.cpp new file mode 100644 index 00000000..b275c36a --- /dev/null +++ b/src/server/PostProcessor.cpp @@ -0,0 +1,114 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cassert> + +#include "Engine.hpp" +#include "Event.hpp" +#include "PostProcessor.hpp" +#include "RunContext.hpp" + +namespace Ingen { +namespace Server { + +class Sentinel : public Event { +public: + Sentinel(Engine& engine) : Event(engine) {} + + bool pre_process(PreProcessContext& ctx) { return false; } + void execute(RunContext& context) {} + void post_process() {} +}; + +PostProcessor::PostProcessor(Engine& engine) + : _engine(engine) + , _head(new Sentinel(engine)) + , _tail(_head.load()) + , _max_time(0) +{ +} + +PostProcessor::~PostProcessor() +{ + /* Delete any straggler events (usually at least one since the event list + is never completely emptied by process()). */ + Event* e = _head; + while (e) { + Event* const next = e->next(); + delete e; + e = next; + } +} + +void +PostProcessor::append(RunContext& context, Event* first, Event* last) +{ + assert(first); + assert(last); + assert(!last->next()); + + // The only place where _tail is written or next links are changed + _tail.load()->next(first); + _tail.store(last); +} + +bool +PostProcessor::pending() const +{ + return _head.load()->next() || _engine.pending_notifications(); +} + +void +PostProcessor::process() +{ + const FrameTime end_time = _max_time; + + /* We can never empty the list and set _head = _tail = NULL since this + would cause a race with append. Instead, head is an already + post-processed node, or initially a sentinel. */ + Event* ev = _head.load(); + Event* next = ev->next(); + if (!next || next->time() >= end_time) { + // Process audio thread notifications until end + _engine.emit_notifications(end_time); + return; + } + + do { + // Delete previously post-processed ev and move to next + delete ev; + ev = next; + + // Process audio thread notifications up until this event's time + _engine.emit_notifications(ev->time()); + + // Post-process event + ev->post_process(); + next = ev->next(); // [1] (see below) + } while (next && next->time() < end_time); + + /* Reached the tail (as far as we're concerned). There may be successors + by now if append() has been called since [1], but that's fine. Now, ev + points to the last post-processed event, which will be the new head. */ + assert(ev); + _head = ev; + + // Process remaining audio thread notifications until end + _engine.emit_notifications(end_time); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/PostProcessor.hpp b/src/server/PostProcessor.hpp new file mode 100644 index 00000000..5a3ffa62 --- /dev/null +++ b/src/server/PostProcessor.hpp @@ -0,0 +1,74 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_POSTPROCESSOR_HPP +#define INGEN_ENGINE_POSTPROCESSOR_HPP + +#include <atomic> + +#include "ingen/ingen.h" + +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class Event; +class RunContext; + +/** Processor for Events after leaving the audio thread. + * + * The audio thread pushes events to this when it is done with them (which + * is realtime-safe), which signals the processing thread through a semaphore + * to handle the event and pass it on to the Maid. + * + * Update: This is all run from main_iteration now to solve scripting + * thread issues. Not sure if this is permanent/ideal or not... + * + * \ingroup engine + */ +class INGEN_API PostProcessor +{ +public: + explicit PostProcessor(Engine& engine); + ~PostProcessor(); + + /** Push a list of events on to the process queue. + realtime-safe, not thread-safe. + */ + void append(RunContext& context, Event* first, Event* last); + + /** Post-process and delete all pending events */ + void process(); + + /** Return true iff any events are pending */ + bool pending() const; + + /** Set the latest event time that should be post-processed */ + void set_end_time(FrameTime time) { _max_time = time; } + +private: + Engine& _engine; + std::atomic<Event*> _head; + std::atomic<Event*> _tail; + std::atomic<FrameTime> _max_time; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_POSTPROCESSOR_HPP diff --git a/src/server/PreProcessContext.hpp b/src/server/PreProcessContext.hpp new file mode 100644 index 00000000..1b57c013 --- /dev/null +++ b/src/server/PreProcessContext.hpp @@ -0,0 +1,84 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PREPROCESSCONTEXT_HPP +#define INGEN_ENGINE_PREPROCESSCONTEXT_HPP + +#include <unordered_set> + +#include "GraphImpl.hpp" + +namespace Raul { class Maid; } + +namespace Ingen { +namespace Server { + +/** Event pre-processing context. + * + * \ingroup engine + */ +class PreProcessContext +{ +public: + typedef std::unordered_set<GraphImpl*> DirtyGraphs; + + /** Return true iff an atomic bundle is currently being pre-processed. */ + bool in_bundle() const { return _in_bundle; } + + /** Set/unset atomic bundle flag. */ + void set_in_bundle(bool b) { _in_bundle = b; } + + /** Return true iff graph should be compiled now (after a change). + * + * This may return false when an atomic bundle is deferring compilation, in + * which case the graph is flagged as dirty for later compilation. + */ + bool must_compile(GraphImpl& graph) { + if (!graph.enabled()) { + return false; + } else if (_in_bundle) { + _dirty_graphs.insert(&graph); + return false; + } else { + return true; + } + } + + /** Compile graph and return the result if necessary. + * + * This may return null when an atomic bundle is deferring compilation, in + * which case the graph is flagged as dirty for later compilation. + */ + MPtr<CompiledGraph> maybe_compile(Raul::Maid& maid, GraphImpl& graph) { + if (must_compile(graph)) { + return compile(maid, graph); + } + return MPtr<CompiledGraph>(); + } + + /** Return all graphs that require compilation after an atomic bundle. */ + const DirtyGraphs& dirty_graphs() const { return _dirty_graphs; } + DirtyGraphs& dirty_graphs() { return _dirty_graphs; } + +private: + DirtyGraphs _dirty_graphs; + bool _in_bundle = false; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PREPROCESSCONTEXT_HPP diff --git a/src/server/PreProcessor.cpp b/src/server/PreProcessor.cpp new file mode 100644 index 00000000..f674284e --- /dev/null +++ b/src/server/PreProcessor.cpp @@ -0,0 +1,248 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <stdexcept> + +#include "ingen/AtomSink.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/World.hpp" + +#include "Engine.hpp" +#include "Event.hpp" +#include "PostProcessor.hpp" +#include "PreProcessContext.hpp" +#include "PreProcessor.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" +#include "UndoStack.hpp" + +namespace Ingen { +namespace Server { + +PreProcessor::PreProcessor(Engine& engine) + : _engine(engine) + , _sem(0) + , _head(nullptr) + , _tail(nullptr) + , _block_state(BlockState::UNBLOCKED) + , _exit_flag(false) + , _thread(&PreProcessor::run, this) +{} + +PreProcessor::~PreProcessor() +{ + if (_thread.joinable()) { + _exit_flag = true; + _sem.post(); + _thread.join(); + } +} + +void +PreProcessor::event(Event* const ev, Event::Mode mode) +{ + // TODO: Probably possible to make this lock-free with CAS + ThreadManager::assert_not_thread(THREAD_IS_REAL_TIME); + std::lock_guard<std::mutex> lock(_mutex); + + assert(!ev->is_prepared()); + assert(!ev->next()); + ev->set_mode(mode); + + /* Note that tail is only used here, not in process(). The head must be + checked first here, since if it is NULL the tail pointer is junk. */ + Event* const head = _head.load(); + if (!head) { + _head = ev; + _tail = ev; + } else { + _tail.load()->next(ev); + _tail = ev; + } + + _sem.post(); +} + +unsigned +PreProcessor::process(RunContext& context, PostProcessor& dest, size_t limit) +{ + Event* const head = _head.load(); + size_t n_processed = 0; + Event* ev = head; + Event* last = ev; + while (ev && ev->is_prepared()) { + switch (_block_state.load()) { + case BlockState::UNBLOCKED: + break; + case BlockState::PRE_BLOCKED: + if (ev->get_execution() == Event::Execution::BLOCK) { + _block_state = BlockState::BLOCKED; + } else if (ev->get_execution() == Event::Execution::ATOMIC) { + _block_state = BlockState::PROCESSING; + } + break; + case BlockState::BLOCKED: + break; + case BlockState::PRE_UNBLOCKED: + assert(ev->get_execution() == Event::Execution::BLOCK); + if (ev->get_execution() == Event::Execution::BLOCK) { + _block_state = BlockState::PROCESSING; + } + break; + case BlockState::PROCESSING: + if (ev->get_execution() == Event::Execution::UNBLOCK) { + _block_state = BlockState::UNBLOCKED; + } + } + + if (_block_state == BlockState::BLOCKED) { + break; // Waiting for PRE_UNBLOCKED + } else if (ev->time() < context.start()) { + ev->set_time(context.start()); // Too late, nudge to context start + } else if (_block_state != BlockState::PROCESSING && + ev->time() >= context.end()) { + break; // Event is for a future cycle + } + + // Execute event + ev->execute(context); + ++n_processed; + + // Unblock pre-processing if this is a non-bundled atomic event + if (ev->get_execution() == Event::Execution::ATOMIC) { + assert(_block_state.load() == BlockState::PROCESSING); + _block_state = BlockState::UNBLOCKED; + } + + // Move to next event + last = ev; + ev = ev->next(); + + if (_block_state != BlockState::PROCESSING && + limit && n_processed >= limit) { + break; + } + } + + if (n_processed > 0) { +#ifndef NDEBUG + Engine& engine = context.engine(); + if (engine.world()->conf().option("trace").get<int32_t>()) { + const uint64_t start = engine.cycle_start_time(context); + const uint64_t end = engine.current_time(); + fprintf(stderr, "Processed %zu events in %u us\n", + n_processed, (unsigned)(end - start)); + } +#endif + + Event* next = (Event*)last->next(); + last->next(nullptr); + dest.append(context, head, last); + + // Since _head was not NULL, we know it hasn't been changed since + _head = next; + + /* If next is NULL, then _tail may now be invalid. However, it would cause + a race to reset _tail here. Instead, append() checks only _head for + emptiness, and resets the tail appropriately. */ + } + + return n_processed; +} + +void +PreProcessor::run() +{ + PreProcessContext ctx; + + UndoStack& undo_stack = *_engine.undo_stack(); + UndoStack& redo_stack = *_engine.redo_stack(); + AtomWriter undo_writer( + _engine.world()->uri_map(), _engine.world()->uris(), undo_stack); + AtomWriter redo_writer( + _engine.world()->uri_map(), _engine.world()->uris(), redo_stack); + + ThreadManager::set_flag(THREAD_PRE_PROCESS); + + Event* back = nullptr; + while (!_exit_flag) { + if (!_sem.timed_wait(std::chrono::seconds(1))) { + continue; + } + + if (!back) { + // Ran off end, find new unprepared back + back = _head; + while (back && back->is_prepared()) { + back = back->next(); + } + } + + Event* const ev = back; + if (!ev) { + continue; + } + + // Set block state before enqueueing event + switch (ev->get_execution()) { + case Event::Execution::NORMAL: + break; + case Event::Execution::ATOMIC: + assert(_block_state == BlockState::UNBLOCKED); + _block_state = BlockState::PRE_BLOCKED; + break; + case Event::Execution::BLOCK: + assert(_block_state == BlockState::UNBLOCKED); + _block_state = BlockState::PRE_BLOCKED; + break; + case Event::Execution::UNBLOCK: + wait_for_block_state(BlockState::BLOCKED); + _block_state = BlockState::PRE_UNBLOCKED; + } + + // Prepare event, allowing it to be processed + assert(!ev->is_prepared()); + if (ev->pre_process(ctx)) { + switch (ev->get_mode()) { + case Event::Mode::NORMAL: + case Event::Mode::REDO: + undo_stack.start_entry(); + ev->undo(undo_writer); + undo_stack.finish_entry(); + // undo_stack.save(stderr); + break; + case Event::Mode::UNDO: + redo_stack.start_entry(); + ev->undo(redo_writer); + redo_stack.finish_entry(); + // redo_stack.save(stderr, "redo"); + break; + } + } + assert(ev->is_prepared()); + + // Wait for process() if necessary + if (ev->get_execution() == Event::Execution::ATOMIC) { + wait_for_block_state(BlockState::UNBLOCKED); + } + + back = (Event*)ev->next(); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/PreProcessor.hpp b/src/server/PreProcessor.hpp new file mode 100644 index 00000000..eb72328e --- /dev/null +++ b/src/server/PreProcessor.hpp @@ -0,0 +1,87 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_PREPROCESSOR_HPP +#define INGEN_ENGINE_PREPROCESSOR_HPP + +#include <atomic> +#include <thread> +#include <mutex> + +#include "raul/Semaphore.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class Event; +class PostProcessor; +class RunContext; + +class PreProcessor +{ +public: + explicit PreProcessor(Engine& engine); + + ~PreProcessor(); + + /** Return true iff no events are enqueued. */ + inline bool empty() const { return !_head.load(); } + + /** Enqueue an event. + * This is safe to call from any non-realtime thread (it locks). + */ + void event(Event* ev, Event::Mode mode); + + /** Process events for a cycle. + * @return The number of events processed. + */ + unsigned process(RunContext& context, + PostProcessor& dest, + size_t limit = 0); + +protected: + void run(); + +private: + enum class BlockState { + UNBLOCKED, ///< Normal, unblocked execution + PRE_BLOCKED, ///< Preprocess thread has enqueued blocking event + BLOCKED, ///< Process thread has reached blocking event + PRE_UNBLOCKED, ///< Preprocess thread has enqueued unblocking event + PROCESSING ///< Process thread is executing all events in-between + }; + + void wait_for_block_state(const BlockState state) { + while (_block_state != state) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + + Engine& _engine; + std::mutex _mutex; + Raul::Semaphore _sem; + std::atomic<Event*> _head; + std::atomic<Event*> _tail; + std::atomic<BlockState> _block_state; + bool _exit_flag; + std::thread _thread; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_PREPROCESSOR_HPP diff --git a/src/server/RunContext.cpp b/src/server/RunContext.cpp new file mode 100644 index 00000000..3ab9d15c --- /dev/null +++ b/src/server/RunContext.cpp @@ -0,0 +1,195 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/Log.hpp" +#include "ingen/URIMap.hpp" + +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "Task.hpp" + +namespace Ingen { +namespace Server { + +struct Notification +{ + inline Notification(PortImpl* p = nullptr, + FrameTime f = 0, + LV2_URID k = 0, + uint32_t s = 0, + LV2_URID t = 0) + : port(p), time(f), key(k), size(s), type(t) + {} + + PortImpl* port; + FrameTime time; + LV2_URID key; + uint32_t size; + LV2_URID type; +}; + +RunContext::RunContext(Engine& engine, + Raul::RingBuffer* event_sink, + unsigned id, + bool threaded) + : _engine(engine) + , _event_sink(event_sink) + , _task(nullptr) + , _thread(threaded ? new std::thread(&RunContext::run, this) : nullptr) + , _id(id) + , _start(0) + , _end(0) + , _offset(0) + , _nframes(0) + , _realtime(true) +{} + +RunContext::RunContext(const RunContext& copy) + : _engine(copy._engine) + , _event_sink(copy._event_sink) + , _task(nullptr) + , _thread(nullptr) + , _id(copy._id) + , _start(copy._start) + , _end(copy._end) + , _offset(copy._offset) + , _nframes(copy._nframes) + , _realtime(copy._realtime) +{} + +bool +RunContext::must_notify(const PortImpl* port) const +{ + return (port->is_monitored() || _engine.broadcaster()->must_broadcast()); +} + +bool +RunContext::notify(LV2_URID key, + FrameTime time, + PortImpl* port, + uint32_t size, + LV2_URID type, + const void* body) +{ + const Notification n(port, time, key, size, type); + if (_event_sink->write_space() < sizeof(n) + size) { + return false; + } + if (_event_sink->write(sizeof(n), &n) != sizeof(n)) { + _engine.log().rt_error("Error writing header to notification ring\n"); + } else if (_event_sink->write(size, body) != size) { + _engine.log().rt_error("Error writing body to notification ring\n"); + } else { + return true; + } + return false; +} + +void +RunContext::emit_notifications(FrameTime end) +{ + const URIs& uris = _engine.buffer_factory()->uris(); + const uint32_t read_space = _event_sink->read_space(); + Notification note; + for (uint32_t i = 0; i < read_space; i += sizeof(note)) { + if (_event_sink->peek(sizeof(note), ¬e) != sizeof(note) || + note.time >= end) { + return; + } + if (_event_sink->read(sizeof(note), ¬e) == sizeof(note)) { + Atom value = _engine.world()->forge().alloc( + note.size, note.type, nullptr); + if (_event_sink->read(note.size, value.get_body()) == note.size) { + i += note.size; + const char* key = _engine.world()->uri_map().unmap_uri(note.key); + if (key) { + _engine.broadcaster()->set_property( + note.port->uri(), URI(key), value); + if (note.port->is_input() && + (note.key == uris.ingen_value || + note.key == uris.midi_binding)) { + // FIXME: not thread safe + note.port->set_property(URI(key), value); + } + } else { + _engine.log().rt_error("Error unmapping notification key URI\n"); + } + } else { + _engine.log().rt_error("Error reading body from notification ring\n"); + } + } else { + _engine.log().rt_error("Error reading header from notification ring\n"); + } + } +} + +void +RunContext::claim_task(Task* task) +{ + if ((_task = task)) { + _engine.signal_tasks_available(); + } +} + +Task* +RunContext::steal_task() const +{ + return _engine.steal_task(_id + 1); +} + +void +RunContext::set_priority(int priority) +{ + if (_thread) { + pthread_t pthread = _thread->native_handle(); + const int policy = (priority > 0) ? SCHED_FIFO : SCHED_OTHER; + sched_param sp; + sp.sched_priority = (priority > 0) ? priority : 0; + if (pthread_setschedparam(pthread, policy, &sp)) { + _engine.log().error( + fmt("Failed to set real-time priority of run thread (%s)\n") + % strerror(errno)); + } + } +} + +void +RunContext::join() +{ + if (_thread) { + if (_thread->joinable()) { + _thread->join(); + } + delete _thread; + } +} + +void +RunContext::run() +{ + while (_engine.wait_for_tasks()) { + for (Task* t; (t = _engine.steal_task(0));) { + t->run(*this); + } + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/RunContext.hpp b/src/server/RunContext.hpp new file mode 100644 index 00000000..bb64a250 --- /dev/null +++ b/src/server/RunContext.hpp @@ -0,0 +1,161 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_RUNCONTEXT_HPP +#define INGEN_ENGINE_RUNCONTEXT_HPP + +#include <cstdint> +#include <thread> + +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "raul/RingBuffer.hpp" + +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class Engine; +class PortImpl; +class Task; + +/** Graph execution context. + * + * This is used to pass whatever information a Node might need to process; such + * as the current time, a sink for generated events, etc. + * + * Note the logical distinction between nframes (jack relative) and start/end + * (timeline relative). If transport speed != 1.0, then end-start != nframes + * (though currently this is never the case, it may be if ingen incorporates + * tempo and varispeed). + * + * \ingroup engine + */ +class RunContext +{ +public: + /** Create a new run context. + * + * @param engine The engine this context is running within. + * @param event_sink Sink for notification events (peaks etc) + * @param id The ID of this context. + * @param threaded If true, then this context is a worker which will launch + * a thread and execute tasks as they become available. + */ + RunContext(Engine& engine, + Raul::RingBuffer* event_sink, + unsigned id, + bool threaded); + + /** Create a sub-context of `parent`. + * + * This is used to subdivide process cycles, the sub-context is + * lightweight and only serves to pass different time attributes. + */ + RunContext(const RunContext& copy); + + /** Return true iff the given port should broadcast its value. + * + * Whether or not broadcasting is actually done is a per-client property, + * this is for use in the audio thread to quickly determine if the + * necessary calculations need to be done at all. + */ + bool must_notify(const PortImpl* port) const; + + /** Send a notification from this run context. + * @return false on failure (ring is full) + */ + bool notify(LV2_URID key = 0, + FrameTime time = 0, + PortImpl* port = nullptr, + uint32_t size = 0, + LV2_URID type = 0, + const void* body = nullptr); + + /** Emit pending notifications in some other non-realtime thread. */ + void emit_notifications(FrameTime end); + + /** Return true iff any notifications are pending. */ + bool pending_notifications() const { return _event_sink->read_space(); } + + /** Return the duration of this cycle in microseconds. + * + * This is the cycle length in frames (nframes) converted to microseconds, + * that is, the amount of real time that this cycle's audio represents. + * Note that this is unrelated to the amount of time available to execute a + * cycle (other than the fact that it must be processed in significantly + * less time to avoid a dropout when running in real time). + */ + inline uint64_t duration() const { + return (uint64_t)_nframes * 1e6 / _rate; + } + + inline void locate(FrameTime s, SampleCount nframes) { + _start = s; + _end = s + nframes; + _nframes = nframes; + } + + inline void slice(SampleCount offset, SampleCount nframes) { + _offset = offset; + _nframes = nframes; + } + + /** Claim a parallel task, and signal others that work is available. */ + void claim_task(Task* task); + + /** Steal a task from some other context if possible. */ + Task* steal_task() const; + + void set_priority(int priority); + void set_rate(SampleCount rate) { _rate = rate; } + + void join(); + + inline Engine& engine() const { return _engine; } + inline Task* task() const { return _task; } + inline unsigned id() const { return _id; } + inline FrameTime start() const { return _start; } + inline FrameTime time() const { return _start + _offset; } + inline FrameTime end() const { return _end; } + inline SampleCount offset() const { return _offset; } + inline SampleCount nframes() const { return _nframes; } + inline SampleCount rate() const { return _rate; } + inline bool realtime() const { return _realtime; } + +protected: + const RunContext& operator=(const RunContext& copy) = delete; + + void run(); + + Engine& _engine; ///< Engine we're running in + Raul::RingBuffer* _event_sink; ///< Port updates from process context + Task* _task; ///< Currently executing task + std::thread* _thread; ///< Thread (NULL for main run context) + unsigned _id; ///< Context ID + + FrameTime _start; ///< Start frame of this cycle, timeline relative + FrameTime _end; ///< End frame of this cycle, timeline relative + SampleCount _offset; ///< Offset into data buffers + SampleCount _nframes; ///< Number of frames past offset to process + SampleCount _rate; ///< Sample rate in Hz + bool _realtime; ///< True iff context is hard realtime +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_RUNCONTEXT_HPP diff --git a/src/server/SocketListener.cpp b/src/server/SocketListener.cpp new file mode 100644 index 00000000..a6faa863 --- /dev/null +++ b/src/server/SocketListener.cpp @@ -0,0 +1,190 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <poll.h> +#include <signal.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <cerrno> +#include <sstream> +#include <string> +#include <thread> + +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/World.hpp" +#include "raul/Socket.hpp" + +#include "../server/Engine.hpp" +#include "../server/EventWriter.hpp" + +#include "SocketListener.hpp" +#include "SocketServer.hpp" + +namespace Ingen { +namespace Server { + +static constexpr const char* const unix_scheme = "unix://"; + +static std::string +get_link_target(const char* link_path) +{ + // Stat the link to get the required size for the target path + struct stat link_stat; + if (lstat(link_path, &link_stat)) { + return std::string(); + } + + // Allocate buffer and read link target + char* target = (char*)calloc(1, link_stat.st_size + 1); + if (readlink(link_path, target, link_stat.st_size) != -1) { + const std::string result(target); + free(target); + return result; + } + + return std::string(); +} + +static void ingen_listen(Engine* engine, + Raul::Socket* unix_sock, + Raul::Socket* net_sock); + + +SocketListener::SocketListener(Engine& engine) + : unix_sock(Raul::Socket::Type::UNIX) + , net_sock(Raul::Socket::Type::TCP) + , thread(new std::thread(ingen_listen, &engine, &unix_sock, &net_sock)) +{} + +SocketListener::~SocketListener() { + unix_sock.shutdown(); + net_sock.shutdown(); + thread->join(); + unlink(unix_sock.uri().substr(strlen(unix_scheme)).c_str()); +} + +static void +ingen_listen(Engine* engine, Raul::Socket* unix_sock, Raul::Socket* net_sock) +{ + Ingen::World* world = engine->world(); + + const std::string link_path(world->conf().option("socket").ptr<char>()); + const std::string unix_path(link_path + "." + std::to_string(getpid())); + + // Bind UNIX socket and create PID-less symbolic link + const URI unix_uri(unix_scheme + unix_path); + bool make_link = true; + if (!unix_sock->bind(unix_uri) || !unix_sock->listen()) { + world->log().error("Failed to create UNIX socket\n"); + unix_sock->close(); + make_link = false; + } else { + const std::string old_path = get_link_target(link_path.c_str()); + if (!old_path.empty()) { + const std::string suffix = old_path.substr(old_path.find_last_of(".") + 1); + const pid_t pid = std::stoi(suffix); + if (!kill(pid, 0)) { + make_link = false; + world->log().warn(fmt("Another Ingen instance is running at %1% => %2%\n") + % link_path % old_path); + } else { + world->log().warn(fmt("Replacing old link %1% => %2%\n") + % link_path % old_path); + unlink(link_path.c_str()); + } + } + + if (make_link) { + if (!symlink(unix_path.c_str(), link_path.c_str())) { + world->log().info(fmt("Listening on %1%\n") % + (unix_scheme + link_path)); + } else { + world->log().error(fmt("Failed to link %1% => %2% (%3%)\n") + % link_path % unix_path % strerror(errno)); + } + } else { + world->log().info(fmt("Listening on %1%\n") % unix_uri); + } + } + + // Bind TCP socket + const int port = world->conf().option("engine-port").get<int32_t>(); + std::ostringstream ss; + ss << "tcp://*:" << port; + if (!net_sock->bind(URI(ss.str())) || !net_sock->listen()) { + world->log().error("Failed to create TCP socket\n"); + net_sock->close(); + } else { + world->log().info(fmt("Listening on TCP port %1%\n") % port); + } + + if (unix_sock->fd() == -1 && net_sock->fd() == -1) { + return; // No sockets to listen to, exit thread + } + + struct pollfd pfds[2]; + int nfds = 0; + if (unix_sock->fd() != -1) { + pfds[nfds].fd = unix_sock->fd(); + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + ++nfds; + } + if (net_sock->fd() != -1) { + pfds[nfds].fd = net_sock->fd(); + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + ++nfds; + } + + while (true) { + // Wait for input to arrive at a socket + const int ret = poll(pfds, nfds, -1); + if (ret == -1) { + world->log().error(fmt("Poll error: %1%\n") % strerror(errno)); + break; + } else if (ret == 0) { + world->log().warn("Poll returned with no data\n"); + continue; + } else if ((pfds[0].revents & POLLHUP) || pfds[1].revents & POLLHUP) { + break; + } + + if (pfds[0].revents & POLLIN) { + SPtr<Raul::Socket> conn = unix_sock->accept(); + if (conn) { + new SocketServer(*world, *engine, conn); + } + } + + if (pfds[1].revents & POLLIN) { + SPtr<Raul::Socket> conn = net_sock->accept(); + if (conn) { + new SocketServer(*world, *engine, conn); + } + } + } + + if (make_link) { + unlink(link_path.c_str()); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/SocketListener.hpp b/src/server/SocketListener.hpp new file mode 100644 index 00000000..e74629ad --- /dev/null +++ b/src/server/SocketListener.hpp @@ -0,0 +1,41 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <memory> +#include <thread> + +#include "raul/Socket.hpp" + +namespace Ingen { +namespace Server { + +class Engine; + +/** Listens on main sockets and spawns socket servers for new connections. */ +class SocketListener +{ +public: + SocketListener(Engine& engine); + ~SocketListener(); + +private: + Raul::Socket unix_sock; + Raul::Socket net_sock; + std::unique_ptr<std::thread> thread; +}; + +} // namespace Server +} // namespace Ingen diff --git a/src/server/SocketServer.hpp b/src/server/SocketServer.hpp new file mode 100644 index 00000000..dbeb76ea --- /dev/null +++ b/src/server/SocketServer.hpp @@ -0,0 +1,80 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_SERVER_SOCKET_SERVER_HPP +#define INGEN_SERVER_SOCKET_SERVER_HPP + +#include "ingen/SocketReader.hpp" +#include "ingen/SocketWriter.hpp" +#include "ingen/StreamWriter.hpp" +#include "ingen/Tee.hpp" +#include "raul/Socket.hpp" + +#include "EventWriter.hpp" + +namespace Ingen { +namespace Server { + +/** The server side of an Ingen socket connection. */ +class SocketServer +{ +public: + SocketServer(World& world, + Server::Engine& engine, + SPtr<Raul::Socket> sock) + : _engine(engine) + , _sink(world.conf().option("dump").get<int32_t>() + ? SPtr<Interface>( + new Tee({SPtr<Interface>(new EventWriter(engine)), + SPtr<Interface>(new StreamWriter(world.uri_map(), + world.uris(), + URI("ingen:/engine"), + stderr, + ColorContext::Color::CYAN))})) + : SPtr<Interface>(new EventWriter(engine))) + , _reader(new SocketReader(world, *_sink.get(), sock)) + , _writer(new SocketWriter(world.uri_map(), + world.uris(), + URI(sock->uri()), + sock)) + { + _sink->set_respondee(_writer); + engine.register_client(_writer); + } + + ~SocketServer() { + if (_writer) { + _engine.unregister_client(_writer); + } + } + +protected: + void on_hangup() { + _engine.unregister_client(_writer); + _writer.reset(); + } + +private: + Server::Engine& _engine; + SPtr<Interface> _sink; + SPtr<SocketReader> _reader; + SPtr<SocketWriter> _writer; +}; + +} // namespace Ingen +} // namespace Socket + +#endif // INGEN_SERVER_SOCKET_SERVER_HPP diff --git a/src/server/Task.cpp b/src/server/Task.cpp new file mode 100644 index 00000000..d2cb2683 --- /dev/null +++ b/src/server/Task.cpp @@ -0,0 +1,158 @@ +/* + This file is part of Ingen. + Copyright 2015-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "BlockImpl.hpp" +#include "Task.hpp" + +namespace Ingen { +namespace Server { + +void +Task::run(RunContext& context) +{ + switch (_mode) { + case Mode::SINGLE: + // fprintf(stderr, "%u run %s\n", context.id(), _block->path().c_str()); + _block->process(context); + break; + case Mode::SEQUENTIAL: + for (const auto& task : _children) { + task->run(context); + } + break; + case Mode::PARALLEL: + // Initialize (not) done state of sub-tasks + for (const auto& task : _children) { + task->set_done(false); + } + + // Grab the first sub-task + _next = 0; + _done_end = 0; + Task* t = steal(context); + + // Allow other threads to steal sub-tasks + context.claim_task(this); + + // Run available tasks until this task is finished + for (; t; t = get_task(context)) { + t->run(context); + } + context.claim_task(nullptr); + break; + } + + set_done(true); +} + +Task* +Task::steal(RunContext& context) +{ + if (_mode == Mode::PARALLEL) { + const unsigned i = _next++; + if (i < _children.size()) { + return _children[i].get(); + } + } + + return nullptr; +} + +Task* +Task::get_task(RunContext& context) +{ + // Attempt to "steal" a task from ourselves + Task* t = steal(context); + if (t) { + return t; + } + + while (true) { + // Push done end index as forward as possible + while (_done_end < _children.size() && _children[_done_end]->done()) { + ++_done_end; + } + + if (_done_end >= _children.size()) { + return nullptr; // All child tasks are finished + } + + // All child tasks claimed, but some are unfinished, steal a task + if ((t = context.steal_task())) { + return t; + } + + /* All child tasks are claimed, and we failed to steal any tasks. Spin + to prevent blocking, though it would probably be wiser to wait for a + signal in non-main threads, and maybe even in the main thread + depending on your real-time safe philosophy... more experimentation + here is needed. */ + } +} + +std::unique_ptr<Task> +Task::simplify(std::unique_ptr<Task>&& task) +{ + if (task->mode() == Mode::SINGLE) { + return std::move(task); + } + + std::unique_ptr<Task> ret = std::unique_ptr<Task>(new Task(task->mode())); + for (auto&& c : task->_children) { + auto child = simplify(std::move(c)); + if (!child->empty()) { + if (child->mode() == task->mode()) { + // Merge child into parent + for (auto&& grandchild : child->_children) { + ret->append(std::move(grandchild)); + } + } else { + // Add child task + ret->append(std::move(child)); + } + } + } + + if (ret->_children.size() == 1) { + return std::move(ret->_children.front()); + } + + return ret; +} + +void +Task::dump(std::function<void (const std::string&)> sink, unsigned indent, bool first) const +{ + if (!first) { + sink("\n"); + for (unsigned i = 0; i < indent; ++i) { + sink(" "); + } + } + + if (_mode == Mode::SINGLE) { + sink(_block->path()); + } else { + sink(((_mode == Mode::SEQUENTIAL) ? "(seq " : "(par ")); + for (size_t i = 0; i < _children.size(); ++i) { + _children[i]->dump(sink, indent + 5, i == 0); + } + sink(")"); + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Task.hpp b/src/server/Task.hpp new file mode 100644 index 00000000..2cdad71b --- /dev/null +++ b/src/server/Task.hpp @@ -0,0 +1,120 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_TASK_HPP +#define INGEN_ENGINE_TASK_HPP + +#include <atomic> +#include <cassert> +#include <deque> +#include <functional> +#include <memory> +#include <ostream> + +namespace Ingen { +namespace Server { + +class BlockImpl; +class RunContext; + +class Task { +public: + enum class Mode { + SINGLE, ///< Single block to run + SEQUENTIAL, ///< Elements must be run sequentially in order + PARALLEL ///< Elements may be run in any order in parallel + }; + + Task(Mode mode, BlockImpl* block = nullptr) + : _block(block) + , _mode(mode) + , _done_end(0) + , _next(0) + , _done(false) + { + assert(!(mode == Mode::SINGLE && !block)); + } + + Task(Task&& task) + : _children(std::move(task._children)) + , _block(task._block) + , _mode(task._mode) + , _done_end(task._done_end) + , _next(task._next.load()) + , _done(task._done.load()) + {} + + Task& operator=(Task&& task) + { + _children = std::move(task._children); + _block = task._block; + _mode = task._mode; + _done_end = task._done_end; + _next = task._next.load(); + _done = task._done.load(); + return *this; + } + + /** Run task in the given context. */ + void run(RunContext& context); + + /** Pretty print task to the given stream (recursively). */ + void dump(std::function<void (const std::string&)> sink, unsigned indent, bool first) const; + + /** Return true iff this is an empty task. */ + bool empty() const { return _mode != Mode::SINGLE && _children.empty(); } + + /** Simplify task expression. */ + static std::unique_ptr<Task> simplify(std::unique_ptr<Task>&& task); + + /** Steal a child task from this task (succeeds for PARALLEL only). */ + Task* steal(RunContext& context); + + /** Prepend a child to this task. */ + void push_front(Task&& task) { + _children.emplace_front(std::unique_ptr<Task>(new Task(std::move(task)))); + } + + Mode mode() const { return _mode; } + BlockImpl* block() const { return _block; } + bool done() const { return _done; } + + void set_done(bool done) { _done = done; } + +private: + typedef std::deque<std::unique_ptr<Task>> Children; + + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + + Task* get_task(RunContext& context); + + void append(std::unique_ptr<Task>&& t) { + _children.emplace_back(std::move(t)); + } + + Children _children; ///< Vector of child tasks + BlockImpl* _block; ///< Used for SINGLE only + Mode _mode; ///< Execution mode + unsigned _done_end; ///< Index of rightmost done sub-task + std::atomic<unsigned> _next; ///< Index of next sub-task + std::atomic<bool> _done; ///< Completion phase +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_TASK_HPP diff --git a/src/server/ThreadManager.hpp b/src/server/ThreadManager.hpp new file mode 100644 index 00000000..3bcedf30 --- /dev/null +++ b/src/server/ThreadManager.hpp @@ -0,0 +1,68 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_THREADMANAGER_HPP +#define INGEN_ENGINE_THREADMANAGER_HPP + +#include <cassert> + +#include "ingen/ingen.h" + +#include "util.hpp" + +namespace Ingen { +namespace Server { + +enum ThreadFlag { + THREAD_IS_REAL_TIME = 1, + THREAD_PRE_PROCESS = 1 << 1, + THREAD_PROCESS = 1 << 2, + THREAD_MESSAGE = 1 << 3, +}; + +class INGEN_API ThreadManager { +public: + static inline void set_flag(ThreadFlag f) { +#ifndef NDEBUG + flags = ((unsigned)flags | f); +#endif + } + + static inline void unset_flag(ThreadFlag f) { +#ifndef NDEBUG + flags = ((unsigned)flags & (~f)); +#endif + } + + static inline void assert_thread(ThreadFlag f) { + assert(single_threaded || (flags & f)); + } + + static inline void assert_not_thread(ThreadFlag f) { + assert(single_threaded || !(flags & f)); + } + + /** Set to true during initialisation so ensure_thread doesn't fail. + * Defined in Engine.cpp + */ + static bool single_threaded; + static INGEN_THREAD_LOCAL unsigned flags; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_THREADMANAGER_HPP diff --git a/src/server/UndoStack.cpp b/src/server/UndoStack.cpp new file mode 100644 index 00000000..dad211ad --- /dev/null +++ b/src/server/UndoStack.cpp @@ -0,0 +1,253 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <ctime> + +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/patch/patch.h" +#include "serd/serd.h" +#include "sratom/sratom.h" + +#include "UndoStack.hpp" + +#define NS_RDF (const uint8_t*)"http://www.w3.org/1999/02/22-rdf-syntax-ns#" + +#define USTR(s) ((const uint8_t*)(s)) + +namespace Ingen { +namespace Server { + +int +UndoStack::start_entry() +{ + if (_depth == 0) { + time_t now; + time(&now); + _stack.push_back(Entry(now)); + } + return ++_depth; +} + +bool +UndoStack::write(const LV2_Atom* msg, int32_t default_id) +{ + _stack.back().push_event(msg); + return true; +} + +bool +UndoStack::ignore_later_event(const LV2_Atom* first, + const LV2_Atom* second) const +{ + if (first->type != _uris.atom_Object || first->type != second->type) { + return false; + } + + const LV2_Atom_Object* f = (const LV2_Atom_Object*)first; + const LV2_Atom_Object* s = (const LV2_Atom_Object*)second; + if (f->body.otype == _uris.patch_Set && f->body.otype == s->body.otype) { + const LV2_Atom* f_subject = nullptr; + const LV2_Atom* f_property = nullptr; + const LV2_Atom* s_subject = nullptr; + const LV2_Atom* s_property = nullptr; + lv2_atom_object_get(f, + (LV2_URID)_uris.patch_subject, &f_subject, + (LV2_URID)_uris.patch_property, &f_property, + 0); + lv2_atom_object_get(s, + (LV2_URID)_uris.patch_subject, &s_subject, + (LV2_URID)_uris.patch_property, &s_property, + 0); + return (lv2_atom_equals(f_subject, s_subject) && + lv2_atom_equals(f_property, s_property)); + } + + return false; +} + +int +UndoStack::finish_entry() +{ + if (--_depth > 0) { + return _depth; + } else if (_stack.back().events.empty()) { + // Disregard empty entry + _stack.pop_back(); + } else if (_stack.size() > 1 && _stack.back().events.size() == 1) { + // This entry and the previous one have one event, attempt to merge + auto i = _stack.rbegin(); + ++i; + if (i->events.size() == 1) { + if (ignore_later_event(i->events[0], _stack.back().events[0])) { + _stack.pop_back(); + } + } + } + + return _depth; +} + +UndoStack::Entry +UndoStack::pop() +{ + Entry top; + if (!_stack.empty()) { + top = _stack.back(); + _stack.pop_back(); + } + return top; +} + +struct BlankIDs { + BlankIDs(char c='b') : n(0), c(c) {} + + SerdNode get() { + snprintf(buf, sizeof(buf), "%c%u", c, n++); + return serd_node_from_string(SERD_BLANK, USTR(buf)); + } + + char buf[16]; + unsigned n; + const char c; +}; + +struct ListContext { + explicit ListContext(BlankIDs& ids, unsigned flags, const SerdNode* s, const SerdNode* p) + : ids(ids) + , s(*s) + , p(*p) + , flags(flags | SERD_LIST_O_BEGIN) + {} + + SerdNode start_node(SerdWriter* writer) { + const SerdNode node = ids.get(); + serd_writer_write_statement(writer, flags, nullptr, &s, &p, &node, nullptr, nullptr); + return node; + } + + void append(SerdWriter* writer, unsigned oflags, const SerdNode* value) { + // s p node + const SerdNode node = start_node(writer); + + // node rdf:first value + p = serd_node_from_string(SERD_URI, NS_RDF "first"); + flags = SERD_LIST_CONT; + serd_writer_write_statement(writer, flags|oflags, nullptr, &node, &p, value, nullptr, nullptr); + + end_node(writer, &node); + } + + void end_node(SerdWriter* writer, const SerdNode* node) { + // Prepare for next call: node rdf:rest ... + s = *node; + p = serd_node_from_string(SERD_URI, NS_RDF "rest"); + } + + void end(SerdWriter* writer) { + const SerdNode nil = serd_node_from_string(SERD_URI, NS_RDF "nil"); + serd_writer_write_statement(writer, flags, nullptr, &s, &p, &nil, nullptr, nullptr); + } + + BlankIDs& ids; + SerdNode s; + SerdNode p; + unsigned flags; +}; + +void +UndoStack::write_entry(Sratom* sratom, + SerdWriter* writer, + const SerdNode* const subject, + const UndoStack::Entry& entry) +{ + char time_str[24]; + strftime(time_str, sizeof(time_str), "%FT%T", gmtime(&entry.time)); + + // entry rdf:type ingen:UndoEntry + SerdNode p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "time")); + SerdNode o = serd_node_from_string(SERD_LITERAL, USTR(time_str)); + serd_writer_write_statement(writer, SERD_ANON_CONT, nullptr, subject, &p, &o, nullptr, nullptr); + + p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "events")); + + BlankIDs ids('e'); + ListContext ctx(ids, SERD_ANON_CONT, subject, &p); + + for (const LV2_Atom* atom : entry.events) { + const SerdNode node = ctx.start_node(writer); + + p = serd_node_from_string(SERD_URI, NS_RDF "first"); + ctx.flags = SERD_LIST_CONT; + sratom_write(sratom, &_map.urid_unmap_feature()->urid_unmap, SERD_LIST_CONT, + &node, &p, + atom->type, atom->size, LV2_ATOM_BODY_CONST(atom)); + + ctx.end_node(writer, &node); + } + + ctx.end(writer); +} + +void +UndoStack::save(FILE* stream, const char* name) +{ + SerdEnv* env = serd_env_new(nullptr); + serd_env_set_prefix_from_strings(env, USTR("atom"), USTR(LV2_ATOM_PREFIX)); + serd_env_set_prefix_from_strings(env, USTR("ingen"), USTR(INGEN_NS)); + serd_env_set_prefix_from_strings(env, USTR("patch"), USTR(LV2_PATCH_PREFIX)); + + const SerdNode base = serd_node_from_string(SERD_URI, USTR("ingen:/")); + SerdURI base_uri; + serd_uri_parse(base.buf, &base_uri); + + SerdWriter* writer = serd_writer_new( + SERD_TURTLE, + (SerdStyle)(SERD_STYLE_RESOLVED|SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED), + env, + &base_uri, + serd_file_sink, + stream); + + // Configure sratom to write directly to the writer (and thus the socket) + Sratom* sratom = sratom_new(&_map.urid_map_feature()->urid_map); + sratom_set_sink(sratom, + (const char*)base.buf, + (SerdStatementSink)serd_writer_write_statement, + (SerdEndSink)serd_writer_end_anon, + writer); + + SerdNode s = serd_node_from_string(SERD_BLANK, (const uint8_t*)name); + SerdNode p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "entries")); + + BlankIDs ids('u'); + ListContext ctx(ids, 0, &s, &p); + for (const Entry& e : _stack) { + const SerdNode entry = ids.get(); + ctx.append(writer, SERD_ANON_O_BEGIN, &entry); + write_entry(sratom, writer, &entry, e); + serd_writer_end_anon(writer, &entry); + } + ctx.end(writer); + + sratom_free(sratom); + serd_writer_finish(writer); + serd_writer_free(writer); +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/UndoStack.hpp b/src/server/UndoStack.hpp new file mode 100644 index 00000000..6ce6475f --- /dev/null +++ b/src/server/UndoStack.hpp @@ -0,0 +1,107 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_UNDOSTACK_HPP +#define INGEN_ENGINE_UNDOSTACK_HPP + +#include <ctime> +#include <deque> + +#include "ingen/AtomSink.hpp" +#include "ingen/ingen.h" +#include "lv2/lv2plug.in/ns/ext/atom/atom.h" +#include "serd/serd.h" +#include "sratom/sratom.h" + +namespace Ingen { + +class URIMap; +class URIs; + +namespace Server { + +class INGEN_API UndoStack : public AtomSink { +public: + struct Entry { + Entry(time_t time=0) : time(time) {} + + Entry(const Entry& copy) + : time(copy.time) + { + for (const LV2_Atom* ev : copy.events) { + push_event(ev); + } + } + + ~Entry() { clear(); } + + Entry& operator=(const Entry& rhs) { + clear(); + time = rhs.time; + for (const LV2_Atom* ev : rhs.events) { + push_event(ev); + } + return *this; + } + + void clear() { + for (LV2_Atom* ev : events) { + free(ev); + } + events.clear(); + } + + void push_event(const LV2_Atom* ev) { + const uint32_t size = lv2_atom_total_size(ev); + LV2_Atom* copy = (LV2_Atom*)malloc(size); + memcpy(copy, ev, size); + events.push_front(copy); + } + + time_t time; + std::deque<LV2_Atom*> events; + }; + + UndoStack(URIs& uris, URIMap& map) : _uris(uris), _map(map), _depth(0) {} + + int start_entry(); + bool write(const LV2_Atom* msg, int32_t default_id=0); + int finish_entry(); + + bool empty() const { return _stack.empty(); } + Entry pop(); + + void save(FILE* stream, const char* name="undo"); + +private: + bool ignore_later_event(const LV2_Atom* first, + const LV2_Atom* second) const; + + void write_entry(Sratom* sratom, + SerdWriter* writer, + const SerdNode* subject, + const Entry& entry); + + URIs& _uris; + URIMap& _map; + std::deque<Entry> _stack; + int _depth; +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_UNDOSTACK_HPP diff --git a/src/server/Worker.cpp b/src/server/Worker.cpp new file mode 100644 index 00000000..6f60250c --- /dev/null +++ b/src/server/Worker.cpp @@ -0,0 +1,163 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/LV2Features.hpp" +#include "ingen/Log.hpp" +#include "lv2/lv2plug.in/ns/ext/worker/worker.h" + +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "LV2Block.hpp" +#include "Worker.hpp" + +namespace Ingen { +namespace Server { + +/// A message in the Worker::_requests ring +struct MessageHeader { + LV2Block* block; ///< Node this message is from + uint32_t size; ///< Size of following data + // `size' bytes of data follow here +}; + +static LV2_Worker_Status +schedule(LV2_Worker_Schedule_Handle handle, + uint32_t size, + const void* data) +{ + LV2Block* block = (LV2Block*)handle; + Engine& engine = block->parent_graph()->engine(); + + return engine.worker()->request(block, size, data); +} + +static LV2_Worker_Status +schedule_sync(LV2_Worker_Schedule_Handle handle, + uint32_t size, + const void* data) +{ + LV2Block* block = (LV2Block*)handle; + Engine& engine = block->parent_graph()->engine(); + + return engine.sync_worker()->request(block, size, data); +} + +LV2_Worker_Status +Worker::request(LV2Block* block, + uint32_t size, + const void* data) +{ + if (_synchronous) { + return block->work(size, data); + } + + Engine& engine = block->parent_graph()->engine(); + if (_requests.write_space() < sizeof(MessageHeader) + size) { + engine.log().error("Work request ring overflow\n"); + return LV2_WORKER_ERR_NO_SPACE; + } + + const MessageHeader msg = { block, size }; + if (_requests.write(sizeof(msg), &msg) != sizeof(msg)) { + engine.log().error("Error writing header to work request ring\n"); + return LV2_WORKER_ERR_UNKNOWN; + } + if (_requests.write(size, data) != size) { + engine.log().error("Error writing body to work request ring\n"); + return LV2_WORKER_ERR_UNKNOWN; + } + + _sem.post(); + + return LV2_WORKER_SUCCESS; +} + +SPtr<LV2_Feature> +Worker::Schedule::feature(World* world, Node* n) +{ + LV2Block* block = dynamic_cast<LV2Block*>(n); + if (!block) { + return SPtr<LV2_Feature>(); + } + + LV2_Worker_Schedule* data = (LV2_Worker_Schedule*)malloc( + sizeof(LV2_Worker_Schedule)); + data->handle = block; + data->schedule_work = synchronous ? schedule_sync : schedule; + + LV2_Feature* f = (LV2_Feature*)malloc(sizeof(LV2_Feature)); + f->URI = LV2_WORKER__schedule; + f->data = data; + + return SPtr<LV2_Feature>(f, &free_feature); +} + +Worker::Worker(Log& log, uint32_t buffer_size, bool synchronous) + : _schedule(new Schedule(synchronous)) + , _log(log) + , _sem(0) + , _requests(buffer_size) + , _responses(buffer_size) + , _buffer((uint8_t*)malloc(buffer_size)) + , _buffer_size(buffer_size) + , _thread(nullptr) + , _exit_flag(false) + , _synchronous(synchronous) +{ + if (!synchronous) { + _thread = new std::thread(&Worker::run, this); + } +} + +Worker::~Worker() +{ + _exit_flag = true; + _sem.post(); + if (_thread) { + _thread->join(); + delete _thread; + } + free(_buffer); +} + +void +Worker::run() +{ + while (_sem.wait() && !_exit_flag) { + MessageHeader msg; + if (_requests.read_space() > sizeof(msg)) { + if (_requests.read(sizeof(msg), &msg) != sizeof(msg)) { + _log.error("Error reading header from work request ring\n"); + continue; + } + + if (msg.size >= _buffer_size - sizeof(msg)) { + _log.error("Corrupt work request ring\n"); + return; + } + + if (_requests.read(msg.size, _buffer) != msg.size) { + _log.error("Error reading body from work request ring\n"); + continue; + } + + msg.block->work(msg.size, _buffer); + } + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/Worker.hpp b/src/server/Worker.hpp new file mode 100644 index 00000000..0a3fdeaf --- /dev/null +++ b/src/server/Worker.hpp @@ -0,0 +1,76 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_WORKER_HPP +#define INGEN_ENGINE_WORKER_HPP + +#include <thread> + +#include "ingen/LV2Features.hpp" +#include "lv2/lv2plug.in/ns/ext/worker/worker.h" +#include "raul/RingBuffer.hpp" +#include "raul/Semaphore.hpp" + +namespace Ingen { + +class Log; + +namespace Server { + +class LV2Block; + +class Worker +{ +public: + Worker(Log& log, uint32_t buffer_size, bool synchronous=false); + ~Worker(); + + struct Schedule : public LV2Features::Feature { + Schedule(bool sync) : synchronous(sync) {} + + const char* uri() const { return LV2_WORKER__schedule; } + + SPtr<LV2_Feature> feature(World* world, Node* n); + + const bool synchronous; + }; + + LV2_Worker_Status request(LV2Block* block, + uint32_t size, + const void* data); + + SPtr<Schedule> schedule_feature() { return _schedule; } + +private: + SPtr<Schedule> _schedule; + + Log& _log; + Raul::Semaphore _sem; + Raul::RingBuffer _requests; + Raul::RingBuffer _responses; + uint8_t* const _buffer; + const uint32_t _buffer_size; + std::thread* _thread; + bool _exit_flag; + bool _synchronous; + + void run(); +}; + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_WORKER_HPP diff --git a/src/server/events.hpp b/src/server/events.hpp new file mode 100644 index 00000000..5f77b431 --- /dev/null +++ b/src/server/events.hpp @@ -0,0 +1,35 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_EVENTS_HPP +#define INGEN_ENGINE_EVENTS_HPP + +#include "events/Connect.hpp" +#include "events/Copy.hpp" +#include "events/CreateBlock.hpp" +#include "events/CreateGraph.hpp" +#include "events/CreatePort.hpp" +#include "events/Delete.hpp" +#include "events/Delta.hpp" +#include "events/Disconnect.hpp" +#include "events/DisconnectAll.hpp" +#include "events/Get.hpp" +#include "events/Mark.hpp" +#include "events/Move.hpp" +#include "events/SetPortValue.hpp" +#include "events/Undo.hpp" + +#endif // INGEN_ENGINE_EVENTS_HPP diff --git a/src/server/events/Connect.cpp b/src/server/events/Connect.cpp new file mode 100644 index 00000000..8937b327 --- /dev/null +++ b/src/server/events/Connect.cpp @@ -0,0 +1,188 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Store.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "ArcImpl.hpp" +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "Connect.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" +#include "internals/BlockDelay.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Connect::Connect(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Connect& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _graph(nullptr) + , _head(nullptr) +{} + +bool +Connect::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + Node* tail = _engine.store()->get(_msg.tail); + if (!tail) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.tail); + } + + Node* head = _engine.store()->get(_msg.head); + if (!head) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.head); + } + + PortImpl* tail_output = dynamic_cast<PortImpl*>(tail); + _head = dynamic_cast<InputPort*>(head); + if (!tail_output || !_head) { + return Event::pre_process_done(Status::BAD_REQUEST, _msg.head); + } + + BlockImpl* const tail_block = tail_output->parent_block(); + BlockImpl* const head_block = _head->parent_block(); + if (!tail_block || !head_block) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, _msg.head); + } + + if (tail_block->parent() != head_block->parent() + && tail_block != head_block->parent() + && tail_block->parent() != head_block) { + return Event::pre_process_done(Status::PARENT_DIFFERS, _msg.head); + } + + if (!ArcImpl::can_connect(tail_output, _head)) { + return Event::pre_process_done(Status::TYPE_MISMATCH, _msg.head); + } + + if (tail_block->parent_graph() != head_block->parent_graph()) { + // Arc to a graph port from inside the graph + assert(tail_block->parent() == head_block || head_block->parent() == tail_block); + if (tail_block->parent() == head_block) { + _graph = dynamic_cast<GraphImpl*>(head_block); + } else { + _graph = dynamic_cast<GraphImpl*>(tail_block); + } + } else if (tail_block == head_block && dynamic_cast<GraphImpl*>(tail_block)) { + // Arc from a graph input to a graph output (pass through) + _graph = dynamic_cast<GraphImpl*>(tail_block); + } else { + // Normal arc between blocks with the same parent + _graph = tail_block->parent_graph(); + } + + if (_graph->has_arc(tail_output, _head)) { + return Event::pre_process_done(Status::EXISTS, _msg.head); + } + + _arc = SPtr<ArcImpl>(new ArcImpl(tail_output, _head)); + + /* Need to be careful about graph port arcs here and adding a + block's parent as a dependant/provider, or adding a graph as its own + provider... + */ + if (tail_block != head_block && tail_block->parent() == head_block->parent()) { + // Connection is between blocks inside a graph, compile graph + + // The tail block is now a dependency (provider) of the head block + head_block->providers().insert(tail_block); + + if (!dynamic_cast<Internals::BlockDelayNode*>(tail_block)) { + /* Arcs leaving a delay node are ignored for the purposes of + compilation, since the output is from the previous cycle and + does not affect execution order. Otherwise, the head block is + now a dependant of the head block. */ + tail_block->dependants().insert(head_block); + } + + if (ctx.must_compile(*_graph)) { + if (!(_compiled_graph = compile(*_engine.maid(), *_graph))) { + head_block->providers().erase(tail_block); + tail_block->dependants().erase(head_block); + return Event::pre_process_done(Status::COMPILATION_FAILED); + } + } + } + + _graph->add_arc(_arc); + _head->increment_num_arcs(); + + if (!_head->is_driver_port()) { + BufferFactory& bufs = *_engine.buffer_factory(); + _voices = bufs.maid().make_managed<PortImpl::Voices>(_head->poly()); + _head->pre_get_buffers(bufs, _voices, _head->poly()); + } + + tail_output->inherit_neighbour(_head, _tail_remove, _tail_add); + _head->inherit_neighbour(tail_output, _head_remove, _head_add); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Connect::execute(RunContext& context) +{ + if (_status == Status::SUCCESS) { + _head->add_arc(context, *_arc.get()); + if (!_head->is_driver_port()) { + _head->set_voices(context, std::move(_voices)); + } + _head->connect_buffers(); + if (_compiled_graph) { + _graph->set_compiled_graph(std::move(_compiled_graph)); + } + } +} + +void +Connect::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + if (!_tail_remove.empty() || !_tail_add.empty()) { + _engine.broadcaster()->delta( + path_to_uri(_msg.tail), _tail_remove, _tail_add); + } + if (!_tail_remove.empty() || !_tail_add.empty()) { + _engine.broadcaster()->delta( + path_to_uri(_msg.tail), _tail_remove, _tail_add); + } + } +} + +void +Connect::undo(Interface& target) +{ + target.disconnect(_msg.tail, _msg.head); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Connect.hpp b/src/server/events/Connect.hpp new file mode 100644 index 00000000..8a42b984 --- /dev/null +++ b/src/server/events/Connect.hpp @@ -0,0 +1,74 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_CONNECT_HPP +#define INGEN_EVENTS_CONNECT_HPP + +#include "raul/Path.hpp" + +#include "CompiledGraph.hpp" +#include "Event.hpp" +#include "PortImpl.hpp" +#include "types.hpp" + +namespace Raul { +template <typename T> class Array; +} + +namespace Ingen { +namespace Server { + +class ArcImpl; +class GraphImpl; +class InputPort; + +namespace Events { + +/** Make an Arc between two Ports. + * + * \ingroup engine + */ +class Connect : public Event +{ +public: + Connect(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Connect& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + const Ingen::Connect _msg; + GraphImpl* _graph; + InputPort* _head; + MPtr<CompiledGraph> _compiled_graph; + SPtr<ArcImpl> _arc; + MPtr<PortImpl::Voices> _voices; + Properties _tail_remove; + Properties _tail_add; + Properties _head_remove; + Properties _head_add; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_CONNECT_HPP diff --git a/src/server/events/Copy.cpp b/src/server/events/Copy.cpp new file mode 100644 index 00000000..fc9d40f7 --- /dev/null +++ b/src/server/events/Copy.cpp @@ -0,0 +1,216 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Parser.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/Store.hpp" +#include "raul/Path.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "Engine.hpp" +#include "EnginePort.hpp" +#include "GraphImpl.hpp" +#include "PreProcessContext.hpp" +#include "events/Copy.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Copy::Copy(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Copy& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _old_block(nullptr) + , _parent(nullptr) + , _block(nullptr) +{} + +bool +Copy::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + if (uri_is_path(_msg.old_uri)) { + // Old URI is a path within the engine + const Raul::Path old_path = uri_to_path(_msg.old_uri); + + // Find the old node + const Store::iterator i = _engine.store()->find(old_path); + if (i == _engine.store()->end()) { + return Event::pre_process_done(Status::NOT_FOUND, old_path); + } + + // Ensure it is a block (ports are not supported for now) + if (!(_old_block = dynamic_ptr_cast<BlockImpl>(i->second))) { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, old_path); + } + + if (uri_is_path(_msg.new_uri)) { + // Copy to path within the engine + return engine_to_engine(ctx); + } else if (_msg.new_uri.scheme() == "file") { + // Copy to filesystem path (i.e. save) + return engine_to_filesystem(ctx); + } else { + return Event::pre_process_done(Status::BAD_REQUEST); + } + } else if (_msg.old_uri.scheme() == "file") { + if (uri_is_path(_msg.new_uri)) { + return filesystem_to_engine(ctx); + } else { + // Ingen is not your file manager + return Event::pre_process_done(Status::BAD_REQUEST); + } + } + + return Event::pre_process_done(Status::BAD_URI); +} + +bool +Copy::engine_to_engine(PreProcessContext& ctx) +{ + // Only support a single source for now + const Raul::Path new_path = uri_to_path(_msg.new_uri); + if (!Raul::Symbol::is_valid(new_path.symbol())) { + return Event::pre_process_done(Status::BAD_REQUEST); + } + + // Ensure the new node doesn't already exist + if (_engine.store()->find(new_path) != _engine.store()->end()) { + return Event::pre_process_done(Status::EXISTS, new_path); + } + + // Find new parent graph + const Raul::Path parent_path = new_path.parent(); + const Store::iterator p = _engine.store()->find(parent_path); + if (p == _engine.store()->end()) { + return Event::pre_process_done(Status::NOT_FOUND, parent_path); + } + if (!(_parent = dynamic_cast<GraphImpl*>(p->second.get()))) { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, parent_path); + } + + // Create new block + if (!(_block = dynamic_cast<BlockImpl*>( + _old_block->duplicate(_engine, Raul::Symbol(new_path.symbol()), _parent)))) { + return Event::pre_process_done(Status::INTERNAL_ERROR); + } + + _block->activate(*_engine.buffer_factory()); + + // Add block to the store and the graph's pre-processor only block list + _parent->add_block(*_block); + _engine.store()->add(_block); + + // Compile graph with new block added for insertion in audio thread + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *_parent); + + return Event::pre_process_done(Status::SUCCESS); +} + +static bool +ends_with(const std::string& str, const std::string& end) +{ + if (str.length() >= end.length()) { + return !str.compare(str.length() - end.length(), end.length(), end); + } + return false; +} + +bool +Copy::engine_to_filesystem(PreProcessContext& ctx) +{ + // Ensure source is a graph + SPtr<GraphImpl> graph = dynamic_ptr_cast<GraphImpl>(_old_block); + if (!graph) { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, _msg.old_uri); + } + + if (!_engine.world()->serialiser()) { + return Event::pre_process_done(Status::INTERNAL_ERROR); + } + + std::lock_guard<std::mutex> lock(_engine.world()->rdf_mutex()); + + if (ends_with(_msg.new_uri, ".ingen") || ends_with(_msg.new_uri, ".ingen/")) { + _engine.world()->serialiser()->write_bundle(graph, URI(_msg.new_uri)); + } else { + _engine.world()->serialiser()->start_to_file(graph->path(), _msg.new_uri); + _engine.world()->serialiser()->serialise(graph); + _engine.world()->serialiser()->finish(); + } + + return Event::pre_process_done(Status::SUCCESS); +} + +bool +Copy::filesystem_to_engine(PreProcessContext& ctx) +{ + if (!_engine.world()->parser()) { + return Event::pre_process_done(Status::INTERNAL_ERROR); + } + + std::lock_guard<std::mutex> lock(_engine.world()->rdf_mutex()); + + // Old URI is a filesystem path and new URI is a path within the engine + const std::string src_path(_msg.old_uri.path()); + const Raul::Path dst_path = uri_to_path(_msg.new_uri); + boost::optional<Raul::Path> dst_parent; + boost::optional<Raul::Symbol> dst_symbol; + if (!dst_path.is_root()) { + dst_parent = dst_path.parent(); + dst_symbol = Raul::Symbol(dst_path.symbol()); + } + + _engine.world()->parser()->parse_file( + _engine.world(), _engine.world()->interface().get(), src_path, + dst_parent, dst_symbol); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Copy::execute(RunContext& context) +{ + if (_block && _compiled_graph) { + _parent->set_compiled_graph(std::move(_compiled_graph)); + } +} + +void +Copy::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + } +} + +void +Copy::undo(Interface& target) +{ + if (uri_is_path(_msg.new_uri)) { + target.del(_msg.new_uri); + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Copy.hpp b/src/server/events/Copy.hpp new file mode 100644 index 00000000..5216b56e --- /dev/null +++ b/src/server/events/Copy.hpp @@ -0,0 +1,68 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_COPY_HPP +#define INGEN_EVENTS_COPY_HPP + +#include <list> + +#include "ingen/Store.hpp" +#include "raul/Path.hpp" + +#include "CompiledGraph.hpp" +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; + +namespace Events { + +/** Copy a graph object to a new path. + * \ingroup engine + */ +class Copy : public Event +{ +public: + Copy(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Copy& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + bool engine_to_engine(PreProcessContext& ctx); + bool engine_to_filesystem(PreProcessContext& ctx); + bool filesystem_to_engine(PreProcessContext& ctx); + + const Ingen::Copy _msg; + SPtr<BlockImpl> _old_block; + GraphImpl* _parent; + BlockImpl* _block; + MPtr<CompiledGraph> _compiled_graph; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_COPY_HPP diff --git a/src/server/events/CreateBlock.cpp b/src/server/events/CreateBlock.cpp new file mode 100644 index 00000000..d678bea3 --- /dev/null +++ b/src/server/events/CreateBlock.cpp @@ -0,0 +1,180 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIs.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "BlockFactory.hpp" +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "CreateBlock.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" +#include "LV2Block.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +CreateBlock::CreateBlock(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + Properties& properties) + : Event(engine, client, id, timestamp) + , _path(path) + , _properties(properties) + , _graph(nullptr) + , _block(nullptr) +{} + +bool +CreateBlock::pre_process(PreProcessContext& ctx) +{ + typedef Properties::const_iterator iterator; + + const Ingen::URIs& uris = _engine.world()->uris(); + const SPtr<Store> store = _engine.store(); + + // Check sanity of target path + if (_path.is_root()) { + return Event::pre_process_done(Status::BAD_URI, _path); + } else if (store->get(_path)) { + return Event::pre_process_done(Status::EXISTS, _path); + } else if (!(_graph = dynamic_cast<GraphImpl*>(store->get(_path.parent())))) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, _path.parent()); + } + + // Map old ingen:prototype to new lv2:prototype + auto range = _properties.equal_range(uris.ingen_prototype); + for (auto i = range.first; i != range.second;) { + const auto value = i->second; + auto next = i; + next = _properties.erase(i); + _properties.emplace(uris.lv2_prototype, value); + i = next; + } + + // Get prototype + iterator t = _properties.find(uris.lv2_prototype); + if (t == _properties.end() || !uris.forge.is_uri(t->second)) { + // Missing/invalid prototype + return Event::pre_process_done(Status::BAD_REQUEST); + } + + const URI prototype(uris.forge.str(t->second, false)); + + // Find polyphony + const iterator p = _properties.find(uris.ingen_polyphonic); + const bool polyphonic = (p != _properties.end() && + p->second.type() == uris.forge.Bool && + p->second.get<int32_t>()); + + // Find and instantiate/duplicate prototype (plugin/existing node) + if (uri_is_path(prototype)) { + // Prototype is an existing block + BlockImpl* const ancestor = dynamic_cast<BlockImpl*>( + store->get(uri_to_path(prototype))); + if (!ancestor) { + return Event::pre_process_done(Status::PROTOTYPE_NOT_FOUND, prototype); + } else if (!(_block = ancestor->duplicate( + _engine, Raul::Symbol(_path.symbol()), _graph))) { + return Event::pre_process_done(Status::CREATION_FAILED, _path); + } + + /* Replace prototype with the ancestor's. This is less informative, + but the client expects an actual LV2 plugin as prototype. */ + _properties.erase(uris.ingen_prototype); + _properties.erase(uris.lv2_prototype); + _properties.emplace(uris.lv2_prototype, + uris.forge.make_urid(ancestor->plugin()->uri())); + } else { + // Prototype is a plugin + PluginImpl* const plugin = _engine.block_factory()->plugin(prototype); + if (!plugin) { + return Event::pre_process_done(Status::PROTOTYPE_NOT_FOUND, prototype); + } + + // Load state from directory if given in properties + LilvState* state = nullptr; + auto s = _properties.find(uris.state_state); + if (s != _properties.end() && s->second.type() == uris.forge.Path) { + state = LV2Block::load_state( + _engine.world(), FilePath(s->second.ptr<char>())); + } + + // Instantiate plugin + if (!(_block = plugin->instantiate(*_engine.buffer_factory(), + Raul::Symbol(_path.symbol()), + polyphonic, + _graph, + _engine, + state))) { + return Event::pre_process_done(Status::CREATION_FAILED, _path); + } + } + + // Activate block + _block->properties().insert(_properties.begin(), _properties.end()); + _block->activate(*_engine.buffer_factory()); + + // Add block to the store and the graph's pre-processor only block list + _graph->add_block(*_block); + store->add(_block); + + /* Compile graph with new block added for insertion in audio thread + TODO: Since the block is not connected at this point, a full compilation + could be avoided and the block simply appended. */ + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *_graph); + + _update.put_block(_block); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +CreateBlock::execute(RunContext& context) +{ + if (_status == Status::SUCCESS && _compiled_graph) { + _graph->set_compiled_graph(std::move(_compiled_graph)); + } +} + +void +CreateBlock::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _update.send(*_engine.broadcaster()); + } +} + +void +CreateBlock::undo(Interface& target) +{ + target.del(_block->uri()); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/CreateBlock.hpp b/src/server/events/CreateBlock.hpp new file mode 100644 index 00000000..0a29e68c --- /dev/null +++ b/src/server/events/CreateBlock.hpp @@ -0,0 +1,66 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_CREATEBLOCK_HPP +#define INGEN_EVENTS_CREATEBLOCK_HPP + +#include "ingen/Resource.hpp" + +#include "ClientUpdate.hpp" +#include "CompiledGraph.hpp" +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; + +namespace Events { + +/** An event to load a Block and insert it into a Graph. + * + * \ingroup engine + */ +class CreateBlock : public Event +{ +public: + CreateBlock(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + Properties& properties); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + Raul::Path _path; + Properties& _properties; + ClientUpdate _update; + GraphImpl* _graph; + BlockImpl* _block; + MPtr<CompiledGraph> _compiled_graph; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_CREATEBLOCK_HPP diff --git a/src/server/events/CreateGraph.cpp b/src/server/events/CreateGraph.cpp new file mode 100644 index 00000000..390fdd9a --- /dev/null +++ b/src/server/events/CreateGraph.cpp @@ -0,0 +1,236 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Forge.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIs.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "Broadcaster.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PreProcessContext.hpp" +#include "events/CreateGraph.hpp" +#include "events/CreatePort.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +CreateGraph::CreateGraph(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + const Properties& properties) + : Event(engine, client, id, timestamp) + , _path(path) + , _properties(properties) + , _graph(nullptr) + , _parent(nullptr) +{} + +CreateGraph::~CreateGraph() +{ + for (Event* ev : _child_events) { + delete ev; + } +} + +void +CreateGraph::build_child_events() +{ + const Ingen::URIs& uris = _engine.world()->uris(); + + // Properties common to both ports + Properties control_properties; + control_properties.put(uris.atom_bufferType, uris.atom_Sequence); + control_properties.put(uris.atom_supports, uris.patch_Message); + control_properties.put(uris.lv2_designation, uris.lv2_control); + control_properties.put(uris.lv2_portProperty, uris.lv2_connectionOptional); + control_properties.put(uris.rdf_type, uris.atom_AtomPort); + control_properties.put(uris.rsz_minimumSize, uris.forge.make(4096)); + + // Add control port (message receive) + Properties in_properties(control_properties); + in_properties.put(uris.lv2_index, uris.forge.make(0)); + in_properties.put(uris.lv2_name, uris.forge.alloc("Control")); + in_properties.put(uris.rdf_type, uris.lv2_InputPort); + in_properties.put(uris.ingen_canvasX, uris.forge.make(32.0f), + Resource::Graph::EXTERNAL); + in_properties.put(uris.ingen_canvasY, uris.forge.make(32.0f), + Resource::Graph::EXTERNAL); + + _child_events.push_back( + new Events::CreatePort( + _engine, _request_client, -1, _time, + _path.child(Raul::Symbol("control")), + in_properties)); + + // Add notify port (message respond) + Properties out_properties(control_properties); + out_properties.put(uris.lv2_index, uris.forge.make(1)); + out_properties.put(uris.lv2_name, uris.forge.alloc("Notify")); + out_properties.put(uris.rdf_type, uris.lv2_OutputPort); + out_properties.put(uris.ingen_canvasX, uris.forge.make(128.0f), + Resource::Graph::EXTERNAL); + out_properties.put(uris.ingen_canvasY, uris.forge.make(32.0f), + Resource::Graph::EXTERNAL); + + _child_events.push_back( + new Events::CreatePort(_engine, _request_client, -1, _time, + _path.child(Raul::Symbol("notify")), + out_properties)); +} + +bool +CreateGraph::pre_process(PreProcessContext& ctx) +{ + if (_engine.store()->get(_path)) { + return Event::pre_process_done(Status::EXISTS, _path); + } + + if (!_path.is_root()) { + const Raul::Path up(_path.parent()); + if (!(_parent = dynamic_cast<GraphImpl*>(_engine.store()->get(up)))) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, up); + } + } + + const Ingen::URIs& uris = _engine.world()->uris(); + + typedef Properties::const_iterator iterator; + + uint32_t ext_poly = 1; + uint32_t int_poly = 1; + iterator p = _properties.find(uris.ingen_polyphony); + if (p != _properties.end() && p->second.type() == uris.forge.Int) { + int_poly = p->second.get<int32_t>(); + } + + if (int_poly < 1 || int_poly > 128) { + return Event::pre_process_done(Status::INVALID_POLY, _path); + } + + if (!_parent || int_poly == _parent->internal_poly()) { + ext_poly = int_poly; + } + + const Raul::Symbol symbol(_path.is_root() ? "graph" : _path.symbol()); + + // Get graph prototype + iterator t = _properties.find(uris.lv2_prototype); + if (t == _properties.end()) { + t = _properties.find(uris.lv2_prototype); + } + + if (t != _properties.end() && + uris.forge.is_uri(t->second) && + URI::is_valid(uris.forge.str(t->second, false)) && + uri_is_path(URI(uris.forge.str(t->second, false)))) { + // Create a duplicate of an existing graph + const URI prototype(uris.forge.str(t->second, false)); + GraphImpl* ancestor = dynamic_cast<GraphImpl*>( + _engine.store()->get(uri_to_path(prototype))); + if (!ancestor) { + return Event::pre_process_done(Status::PROTOTYPE_NOT_FOUND, prototype); + } else if (!(_graph = dynamic_cast<GraphImpl*>( + ancestor->duplicate(_engine, symbol, _parent)))) { + return Event::pre_process_done(Status::CREATION_FAILED, _path); + } + } else { + // Create a new graph + _graph = new GraphImpl(_engine, symbol, ext_poly, _parent, + _engine.sample_rate(), int_poly); + _graph->add_property(uris.rdf_type, uris.ingen_Graph.urid); + _graph->add_property(uris.rdf_type, + Property(uris.ingen_Block, + Resource::Graph::EXTERNAL)); + } + + _graph->set_properties(_properties); + + if (_parent) { + // Add graph to parent + _parent->add_block(*_graph); + if (_parent->enabled()) { + _graph->enable(); + } + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *_parent); + } + + _graph->activate(*_engine.buffer_factory()); + + // Insert into store and build update to send to clients + _engine.store()->add(_graph); + _update.put_graph(_graph); + for (BlockImpl& block : _graph->blocks()) { + _engine.store()->add(&block); + } + + // Build and pre-process child events to create standard ports + build_child_events(); + for (Event* ev : _child_events) { + ev->pre_process(ctx); + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +CreateGraph::execute(RunContext& context) +{ + if (_graph) { + if (_parent) { + if (_compiled_graph) { + _parent->set_compiled_graph(std::move(_compiled_graph)); + } + } else { + _engine.set_root_graph(_graph); + _graph->enable(); + } + + for (Event* ev : _child_events) { + ev->execute(context); + } + } +} + +void +CreateGraph::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _update.send(*_engine.broadcaster()); + } + + if (_graph) { + for (Event* ev : _child_events) { + ev->post_process(); + } + } +} + +void +CreateGraph::undo(Interface& target) +{ + target.del(_graph->uri()); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/CreateGraph.hpp b/src/server/events/CreateGraph.hpp new file mode 100644 index 00000000..564d553b --- /dev/null +++ b/src/server/events/CreateGraph.hpp @@ -0,0 +1,74 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_CREATEGRAPH_HPP +#define INGEN_EVENTS_CREATEGRAPH_HPP + +#include <list> + +#include "ingen/Resource.hpp" + +#include "CompiledGraph.hpp" +#include "Event.hpp" +#include "events/Get.hpp" + +namespace Ingen { +namespace Server { + +class GraphImpl; + +namespace Events { + +/** Creates a new Graph. + * + * \ingroup engine + */ +class CreateGraph : public Event +{ +public: + CreateGraph(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + const Properties& properties); + + ~CreateGraph(); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + + GraphImpl* graph() { return _graph; } + +private: + void build_child_events(); + + const Raul::Path _path; + Properties _properties; + ClientUpdate _update; + GraphImpl* _graph; + GraphImpl* _parent; + MPtr<CompiledGraph> _compiled_graph; + std::list<Event*> _child_events; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_CREATEGRAPH_HPP diff --git a/src/server/events/CreatePort.cpp b/src/server/events/CreatePort.cpp new file mode 100644 index 00000000..e17b8b01 --- /dev/null +++ b/src/server/events/CreatePort.cpp @@ -0,0 +1,219 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <utility> + +#include "ingen/Atom.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/URIs.hpp" +#include "raul/Array.hpp" +#include "raul/Path.hpp" + +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "CreatePort.hpp" +#include "Driver.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +CreatePort::CreatePort(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + const Properties& properties) + : Event(engine, client, id, timestamp) + , _path(path) + , _port_type(PortType::UNKNOWN) + , _buf_type(0) + , _graph(nullptr) + , _graph_port(nullptr) + , _engine_port(nullptr) + , _properties(properties) +{ + const Ingen::URIs& uris = _engine.world()->uris(); + + typedef Properties::const_iterator Iterator; + typedef std::pair<Iterator, Iterator> Range; + + const Range types = properties.equal_range(uris.rdf_type); + for (Iterator i = types.first; i != types.second; ++i) { + const Atom& type = i->second; + if (type == uris.lv2_AudioPort) { + _port_type = PortType::AUDIO; + } else if (type == uris.lv2_ControlPort) { + _port_type = PortType::CONTROL; + } else if (type == uris.lv2_CVPort) { + _port_type = PortType::CV; + } else if (type == uris.atom_AtomPort) { + _port_type = PortType::ATOM; + } else if (type == uris.lv2_InputPort) { + _flow = Flow::INPUT; + } else if (type == uris.lv2_OutputPort) { + _flow = Flow::OUTPUT; + } + } + + const Range buffer_types = properties.equal_range(uris.atom_bufferType); + for (Iterator i = buffer_types.first; i != buffer_types.second; ++i) { + if (uris.forge.is_uri(i->second)) { + _buf_type = _engine.world()->uri_map().map_uri( + uris.forge.str(i->second, false)); + } + } +} + +bool +CreatePort::pre_process(PreProcessContext& ctx) +{ + if (_port_type == PortType::UNKNOWN) { + return Event::pre_process_done(Status::UNKNOWN_TYPE, _path); + } else if (!_flow) { + return Event::pre_process_done(Status::UNKNOWN_TYPE, _path); + } else if (_path.is_root()) { + return Event::pre_process_done(Status::BAD_URI, _path); + } else if (_engine.store()->get(_path)) { + return Event::pre_process_done(Status::EXISTS, _path); + } + + const Raul::Path parent_path = _path.parent(); + Node* const parent = _engine.store()->get(parent_path); + if (!parent) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, parent_path); + } else if (!(_graph = dynamic_cast<GraphImpl*>(parent))) { + return Event::pre_process_done(Status::INVALID_PARENT, parent_path); + } else if (!_graph->parent() && _engine.activated() && + !_engine.driver()->dynamic_ports()) { + return Event::pre_process_done(Status::CREATION_FAILED, _path); + } + + const URIs& uris = _engine.world()->uris(); + BufferFactory& bufs = *_engine.buffer_factory(); + const uint32_t buf_size = bufs.default_size(_buf_type); + const int32_t old_n_ports = _graph->num_ports_non_rt(); + + typedef Properties::const_iterator PropIter; + + PropIter index_i = _properties.find(uris.lv2_index); + int32_t index = 0; + if (index_i != _properties.end()) { + // Ensure given index is sane and not taken + if (index_i->second.type() != uris.forge.Int) { + return Event::pre_process_done(Status::BAD_REQUEST); + } + + index = index_i->second.get<int32_t>(); + if (_graph->has_port_with_index(index)) { + return Event::pre_process_done(Status::BAD_INDEX); + } + } else { + // No index given, append + index = old_n_ports; + index_i = _properties.emplace(uris.lv2_index, + _engine.world()->forge().make(index)); + } + + const PropIter poly_i = _properties.find(uris.ingen_polyphonic); + const bool polyphonic = (poly_i != _properties.end() && + poly_i->second.type() == uris.forge.Bool && + poly_i->second.get<int32_t>()); + + // Create 0 value if the port requires one + Atom value; + if (_port_type == PortType::CONTROL || _port_type == PortType::CV) { + value = bufs.forge().make(0.0f); + } + + // Create port + _graph_port = new DuplexPort(bufs, _graph, Raul::Symbol(_path.symbol()), + index, + polyphonic, + _port_type, _buf_type, buf_size, + value, _flow == Flow::OUTPUT); + assert((_flow == Flow::OUTPUT && _graph_port->is_output()) || + (_flow == Flow::INPUT && _graph_port->is_input())); + _graph_port->properties().insert(_properties.begin(), _properties.end()); + + _engine.store()->add(_graph_port); + if (_flow == Flow::OUTPUT) { + _graph->add_output(*_graph_port); + } else { + _graph->add_input(*_graph_port); + } + + if (!_graph->parent()) { + _engine_port = _engine.driver()->create_port(_graph_port); + } + + _ports_array = bufs.maid().make_managed<GraphImpl::Ports>( + old_n_ports + 1, nullptr); + + _update = _graph_port->properties(); + + assert(_graph_port->index() == (uint32_t)index_i->second.get<int32_t>()); + assert(_graph->num_ports_non_rt() == (uint32_t)old_n_ports + 1); + assert(_ports_array->size() == _graph->num_ports_non_rt()); + assert(_graph_port->index() < _ports_array->size()); + return Event::pre_process_done(Status::SUCCESS); +} + +void +CreatePort::execute(RunContext& context) +{ + if (_status == Status::SUCCESS) { + const MPtr<GraphImpl::Ports>& old_ports = _graph->external_ports(); + if (old_ports) { + for (uint32_t i = 0; i < old_ports->size(); ++i) { + const auto* const old_port = (*old_ports)[i]; + assert(old_port->index() < _ports_array->size()); + (*_ports_array)[old_port->index()] = (*old_ports)[i]; + } + } + assert(!(*_ports_array)[_graph_port->index()]); + (*_ports_array)[_graph_port->index()] = _graph_port; + _graph->set_external_ports(std::move(_ports_array)); + + if (_engine_port) { + _engine.driver()->add_port(context, _engine_port); + } + } +} + +void +CreatePort::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->put(path_to_uri(_path), _update); + } +} + +void +CreatePort::undo(Interface& target) +{ + target.del(_graph_port->uri()); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/CreatePort.hpp b/src/server/events/CreatePort.hpp new file mode 100644 index 00000000..a2ea7682 --- /dev/null +++ b/src/server/events/CreatePort.hpp @@ -0,0 +1,82 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_CREATEPORT_HPP +#define INGEN_EVENTS_CREATEPORT_HPP + +#include <boost/optional.hpp> + +#include "ingen/Resource.hpp" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "raul/Array.hpp" +#include "raul/Path.hpp" + +#include "BlockImpl.hpp" +#include "Event.hpp" +#include "PortType.hpp" + +namespace Ingen { +namespace Server { + +class DuplexPort; +class EnginePort; +class GraphImpl; +class PortImpl; + +namespace Events { + +/** An event to add a Port to a Graph. + * + * \ingroup engine + */ +class CreatePort : public Event +{ +public: + CreatePort(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + const Raul::Path& path, + const Properties& properties); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + enum class Flow { + INPUT, + OUTPUT + }; + + Raul::Path _path; + PortType _port_type; + LV2_URID _buf_type; + GraphImpl* _graph; + DuplexPort* _graph_port; + MPtr<BlockImpl::Ports> _ports_array; ///< New external port array for Graph + EnginePort* _engine_port; ///< Driver port if on the root + Properties _properties; + Properties _update; + boost::optional<Flow> _flow; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_CREATEPORT_HPP diff --git a/src/server/events/Delete.cpp b/src/server/events/Delete.cpp new file mode 100644 index 00000000..e8f9582c --- /dev/null +++ b/src/server/events/Delete.cpp @@ -0,0 +1,216 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Store.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "ControlBindings.hpp" +#include "Delete.hpp" +#include "DisconnectAll.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "EnginePort.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Delete::Delete(Engine& engine, + SPtr<Interface> client, + FrameTime timestamp, + const Ingen::Del& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _engine_port(nullptr) + , _disconnect_event(nullptr) +{ + if (uri_is_path(msg.uri)) { + _path = uri_to_path(msg.uri); + } +} + +Delete::~Delete() +{ + delete _disconnect_event; + for (ControlBindings::Binding* b : _removed_bindings) { + delete b; + } +} + +bool +Delete::pre_process(PreProcessContext& ctx) +{ + const Ingen::URIs& uris = _engine.world()->uris(); + if (_path.is_root() || _path == "/control" || _path == "/notify") { + return Event::pre_process_done(Status::NOT_DELETABLE, _path); + } + + _engine.control_bindings()->get_all(_path, _removed_bindings); + + auto iter = _engine.store()->find(_path); + if (iter == _engine.store()->end()) { + return Event::pre_process_done(Status::NOT_FOUND, _path); + } + + if (!(_block = dynamic_ptr_cast<BlockImpl>(iter->second))) { + _port = dynamic_ptr_cast<DuplexPort>(iter->second); + } + + if ((!_block && !_port) || (_port && !_engine.driver()->dynamic_ports())) { + return Event::pre_process_done(Status::NOT_DELETABLE, _path); + } + + GraphImpl* parent = _block ? _block->parent_graph() : _port->parent_graph(); + if (!parent) { + return Event::pre_process_done(Status::INTERNAL_ERROR, _path); + } + + // Take a writer lock while we modify the store + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + _engine.store()->remove(iter, _removed_objects); + + if (_block) { + parent->remove_block(*_block); + _disconnect_event = new DisconnectAll(_engine, parent, _block.get()); + _disconnect_event->pre_process(ctx); + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *parent); + } else if (_port) { + parent->remove_port(*_port); + _disconnect_event = new DisconnectAll(_engine, parent, _port.get()); + _disconnect_event->pre_process(ctx); + + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *parent); + if (parent->enabled()) { + _ports_array = parent->build_ports_array(*_engine.maid()); + assert(_ports_array->size() == parent->num_ports_non_rt()); + + // Adjust port indices if necessary and record changes for later + for (size_t i = 0; i < _ports_array->size(); ++i) { + PortImpl* const port = _ports_array->at(i); + if (port->index() != i) { + _port_index_changes.emplace( + port->path(), std::make_pair(port->index(), i)); + port->remove_property(uris.lv2_index, uris.patch_wildcard); + port->set_property( + uris.lv2_index, + _engine.buffer_factory()->forge().make((int32_t)i)); + } + } + } + + if (!parent->parent()) { + _engine_port = _engine.driver()->get_port(_port->path()); + } + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Delete::execute(RunContext& context) +{ + if (_status != Status::SUCCESS) { + return; + } + + if (_disconnect_event) { + _disconnect_event->execute(context); + } + + if (!_removed_bindings.empty()) { + _engine.control_bindings()->remove(context, _removed_bindings); + } + + GraphImpl* parent = _block ? _block->parent_graph() : nullptr; + if (_port) { + // Adjust port indices if necessary + for (size_t i = 0; i < _ports_array->size(); ++i) { + PortImpl* const port = _ports_array->at(i); + if (port->index() != i) { + port->set_index(context, i); + } + } + + // Replace ports array in graph + parent = _port->parent_graph(); + parent->set_external_ports(std::move(_ports_array)); + + if (_engine_port) { + _engine.driver()->remove_port(context, _engine_port); + } + } + + if (parent && _compiled_graph) { + parent->set_compiled_graph(std::move(_compiled_graph)); + } +} + +void +Delete::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS && (_block || _port)) { + if (_block) { + _block->deactivate(); + } + + _engine.broadcaster()->message(_msg); + } + + if (_engine_port) { + _engine.driver()->unregister_port(*_engine_port); + delete _engine_port; + } +} + +void +Delete::undo(Interface& target) +{ + const Ingen::URIs& uris = _engine.world()->uris(); + Ingen::Forge& forge = _engine.buffer_factory()->forge(); + + auto i = _removed_objects.find(_path); + if (i != _removed_objects.end()) { + // Undo disconnect + if (_disconnect_event) { + _disconnect_event->undo(target); + } + + // Put deleted item back + target.put(_msg.uri, i->second->properties()); + + // Adjust port indices + for (const auto& c : _port_index_changes) { + if (c.first != _msg.uri.path()) { + target.set_property(path_to_uri(c.first), + uris.lv2_index, + forge.make(int32_t(c.second.first))); + } + } + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Delete.hpp b/src/server/events/Delete.hpp new file mode 100644 index 00000000..8b2a0a91 --- /dev/null +++ b/src/server/events/Delete.hpp @@ -0,0 +1,86 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_DELETE_HPP +#define INGEN_EVENTS_DELETE_HPP + +#include <map> +#include <vector> + +#include "ingen/Store.hpp" + +#include "CompiledGraph.hpp" +#include "ControlBindings.hpp" +#include "Event.hpp" +#include "GraphImpl.hpp" + +namespace Raul { +template<typename T> class Array; +} + +namespace Ingen { +namespace Server { + +class BlockImpl; +class DuplexPort; +class EnginePort; +class PortImpl; + +namespace Events { + +class DisconnectAll; + +/** Delete a graph object. + * \ingroup engine + */ +class Delete : public Event +{ +public: + Delete(Engine& engine, + SPtr<Interface> client, + FrameTime timestamp, + const Ingen::Del& msg); + + ~Delete(); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + using IndexChange = std::pair<uint32_t, uint32_t>; + using IndexChanges = std::map<Raul::Path, IndexChange>; + + const Ingen::Del _msg; + Raul::Path _path; + SPtr<BlockImpl> _block; ///< Non-NULL iff a block + SPtr<DuplexPort> _port; ///< Non-NULL iff a port + EnginePort* _engine_port; + MPtr<GraphImpl::Ports> _ports_array; ///< New (external) ports for Graph + MPtr<CompiledGraph> _compiled_graph; ///< Graph's new process order + DisconnectAll* _disconnect_event; + Store::Objects _removed_objects; + IndexChanges _port_index_changes; + + std::vector<ControlBindings::Binding*> _removed_bindings; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_DELETE_HPP diff --git a/src/server/events/Delta.cpp b/src/server/events/Delta.cpp new file mode 100644 index 00000000..b23ae884 --- /dev/null +++ b/src/server/events/Delta.cpp @@ -0,0 +1,670 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <vector> +#include <thread> + +#include "ingen/Log.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIs.hpp" +#include "raul/Maid.hpp" + +#include "Broadcaster.hpp" +#include "ControlBindings.hpp" +#include "CreateBlock.hpp" +#include "CreateGraph.hpp" +#include "CreatePort.hpp" +#include "Delta.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" +#include "PortType.hpp" +#include "SetPortValue.hpp" +#include "events/Get.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Delta::Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Put& msg) + : Event(engine, client, msg.seq, timestamp) + , _create_event(nullptr) + , _subject(msg.uri) + , _properties(msg.properties) + , _object(nullptr) + , _graph(nullptr) + , _binding(nullptr) + , _state(nullptr) + , _context(msg.ctx) + , _type(Type::PUT) + , _block(false) +{ + init(); +} + +Delta::Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Delta& msg) + : Event(engine, client, msg.seq, timestamp) + , _create_event(nullptr) + , _subject(msg.uri) + , _properties(msg.add) + , _remove(msg.remove) + , _object(nullptr) + , _graph(nullptr) + , _binding(nullptr) + , _state(nullptr) + , _context(msg.ctx) + , _type(Type::PATCH) + , _block(false) +{ + init(); +} + +Delta::Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::SetProperty& msg) + : Event(engine, client, msg.seq, timestamp) + , _create_event(nullptr) + , _subject(msg.subject) + , _properties{{msg.predicate, msg.value}} + , _object(nullptr) + , _graph(nullptr) + , _binding(nullptr) + , _state(nullptr) + , _context(msg.ctx) + , _type(Type::SET) + , _block(false) +{ + init(); +} + +Delta::~Delta() +{ + for (auto& s : _set_events) { + delete s; + } + + delete _create_event; +} + +void +Delta::init() +{ + if (_context != Resource::Graph::DEFAULT) { + for (auto& p : _properties) { + p.second.set_context(_context); + } + } + + // Set atomic execution if polyphony is to be changed + const Ingen::URIs& uris = _engine.world()->uris(); + if (_properties.count(uris.ingen_polyphonic) || + _properties.count(uris.ingen_polyphony)) { + _block = true; + } +} + +void +Delta::add_set_event(const char* port_symbol, + const void* value, + uint32_t size, + uint32_t type) +{ + BlockImpl* block = dynamic_cast<BlockImpl*>(_object); + PortImpl* port = block->port_by_symbol(port_symbol); + if (!port) { + _engine.log().warn(fmt("Unknown port `%1%' in state") % port_symbol); + return; + } + + SetPortValue* ev = new SetPortValue( + _engine, _request_client, _request_id, _time, + port, Atom(size, type, value), false, true); + + _set_events.push_back(ev); +} + +static void +s_add_set_event(const char* port_symbol, + void* user_data, + const void* value, + uint32_t size, + uint32_t type) +{ + ((Delta*)user_data)->add_set_event(port_symbol, value, size, type); +} + +static LilvNode* +get_file_node(LilvWorld* lworld, const URIs& uris, const Atom& value) +{ + if (value.type() == uris.atom_Path) { + return lilv_new_file_uri(lworld, nullptr, value.ptr<char>()); + } else if (uris.forge.is_uri(value)) { + const std::string str = uris.forge.str(value, false); + if (str.substr(0, 5) == "file:") { + return lilv_new_uri(lworld, value.ptr<char>()); + } + } + return nullptr; +} + +bool +Delta::pre_process(PreProcessContext& ctx) +{ + const Ingen::URIs& uris = _engine.world()->uris(); + + const bool is_graph_object = uri_is_path(_subject); + const bool is_client = (_subject == "ingen:/clients/this"); + const bool is_engine = (_subject == "ingen:/"); + const bool is_file = (_subject.scheme() == "file"); + + if (_type == Type::PUT && is_file) { + // Ensure type is Preset, the only supported file put + const auto t = _properties.find(uris.rdf_type); + if (t == _properties.end() || t->second != uris.pset_Preset) { + return Event::pre_process_done(Status::BAD_REQUEST, _subject); + } + + // Get "prototype" for preset (node to save state for) + const auto p = _properties.find(uris.lv2_prototype); + if (p == _properties.end()) { + return Event::pre_process_done(Status::BAD_REQUEST, _subject); + } else if (!_engine.world()->forge().is_uri(p->second)) { + return Event::pre_process_done(Status::BAD_REQUEST, _subject); + } + + const URI prot(_engine.world()->forge().str(p->second, false)); + if (!uri_is_path(prot)) { + return Event::pre_process_done(Status::BAD_URI, _subject); + } + + Node* node = _engine.store()->get(uri_to_path(prot)); + if (!node) { + return Event::pre_process_done(Status::NOT_FOUND, prot); + } + + BlockImpl* block = dynamic_cast<BlockImpl*>(node); + if (!block) { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, prot); + } + + if ((_preset = block->save_preset(_subject, _properties))) { + return Event::pre_process_done(Status::SUCCESS); + } else { + return Event::pre_process_done(Status::FAILURE); + } + } + + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + _object = is_graph_object + ? static_cast<Ingen::Resource*>(_engine.store()->get(uri_to_path(_subject))) + : static_cast<Ingen::Resource*>(_engine.block_factory()->plugin(_subject)); + + if (!_object && !is_client && !is_engine && + (!is_graph_object || _type != Type::PUT)) { + return Event::pre_process_done(Status::NOT_FOUND, _subject); + } + + if (is_graph_object && !_object) { + Raul::Path path(uri_to_path(_subject)); + bool is_graph = false, is_block = false, is_port = false, is_output = false; + Ingen::Resource::type(uris, _properties, is_graph, is_block, is_port, is_output); + + if (is_graph) { + _create_event = new CreateGraph( + _engine, _request_client, _request_id, _time, path, _properties); + } else if (is_block) { + _create_event = new CreateBlock( + _engine, _request_client, _request_id, _time, path, _properties); + } else if (is_port) { + _create_event = new CreatePort( + _engine, _request_client, _request_id, _time, + path, _properties); + } + if (_create_event) { + if (_create_event->pre_process(ctx)) { + _object = _engine.store()->get(path); // Get object for setting + } else { + return Event::pre_process_done(Status::CREATION_FAILED, _subject); + } + } else { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, _subject); + } + } + + _types.reserve(_properties.size()); + + NodeImpl* obj = dynamic_cast<NodeImpl*>(_object); + + // Remove any properties removed in delta + for (const auto& r : _remove) { + const URI& key = r.first; + const Atom& value = r.second; + if (key == uris.midi_binding && value == uris.patch_wildcard) { + PortImpl* port = dynamic_cast<PortImpl*>(_object); + if (port) { + _engine.control_bindings()->get_all(port->path(), _removed_bindings); + } + } + if (_object) { + _removed.emplace(key, value); + _object->remove_property(key, value); + } else if (is_engine && key == uris.ingen_loadedBundle) { + LilvWorld* lworld = _engine.world()->lilv_world(); + LilvNode* bundle = get_file_node(lworld, uris, value); + if (bundle) { + for (const auto& p : _engine.block_factory()->plugins()) { + if (p.second->bundle_uri() == lilv_node_as_string(bundle)) { + p.second->set_is_zombie(true); + _update.del(p.second->uri()); + } + } + lilv_world_unload_bundle(lworld, bundle); + _engine.block_factory()->refresh(); + lilv_node_free(bundle); + } else { + _status = Status::BAD_VALUE; + } + } + } + + // Remove all added properties if this is a put or set + if (_object && (_type == Type::PUT || _type == Type::SET)) { + for (auto p = _properties.begin(); + p != _properties.end(); + p = _properties.upper_bound(p->first)) { + for (auto q = _object->properties().find(p->first); + q != _object->properties().end() && q->first == p->first;) { + auto next = q; + ++next; + + if (!_properties.contains(q->first, q->second)) { + const auto r = std::make_pair(q->first, q->second); + _object->properties().erase(q); + _object->on_property_removed(r.first, r.second); + _removed.insert(r); + } + + q = next; + } + } + } + + for (const auto& p : _properties) { + const URI& key = p.first; + const Property& value = p.second; + SpecialType op = SpecialType::NONE; + if (obj) { + Resource& resource = *obj; + if (value != uris.patch_wildcard) { + if (resource.add_property(key, value, value.context())) { + _added.emplace(key, value); + } + } + + BlockImpl* block = nullptr; + PortImpl* port = dynamic_cast<PortImpl*>(_object); + if (port) { + if (key == uris.ingen_broadcast) { + if (value.type() == uris.forge.Bool) { + op = SpecialType::ENABLE_BROADCAST; + } else { + _status = Status::BAD_VALUE_TYPE; + } + } else if (key == uris.ingen_value || key == uris.ingen_activity) { + SetPortValue* ev = new SetPortValue( + _engine, _request_client, _request_id, _time, port, value, + key == uris.ingen_activity); + _set_events.push_back(ev); + } else if (key == uris.midi_binding) { + if (port->is_a(PortType::CONTROL) || port->is_a(PortType::CV)) { + if (value == uris.patch_wildcard) { + _engine.control_bindings()->start_learn(port); + } else if (value.type() == uris.atom_Object) { + op = SpecialType::CONTROL_BINDING; + _binding = new ControlBindings::Binding(); + } else { + _status = Status::BAD_VALUE_TYPE; + } + } else { + _status = Status::BAD_OBJECT_TYPE; + } + } else if (key == uris.lv2_index) { + op = SpecialType::PORT_INDEX; + port->set_property(key, value); + } + } else if ((block = dynamic_cast<BlockImpl*>(_object))) { + if (key == uris.midi_binding && value == uris.patch_wildcard) { + op = SpecialType::CONTROL_BINDING; // Internal block learn + } else if (key == uris.ingen_enabled) { + if (value.type() == uris.forge.Bool) { + op = SpecialType::ENABLE; + } else { + _status = Status::BAD_VALUE_TYPE; + } + } else if (key == uris.pset_preset) { + URI uri; + if (uris.forge.is_uri(value)) { + const std::string uri_str = uris.forge.str(value, false); + if (URI::is_valid(uri_str)) { + uri = URI(uri_str); + } + } else if (value.type() == uris.forge.Path) { + uri = URI(FilePath(value.ptr<char>())); + } + + if (!uri.empty()) { + op = SpecialType::PRESET; + if ((_state = block->load_preset(uri))) { + lilv_state_emit_port_values( + _state, s_add_set_event, this); + } else { + _engine.log().warn(fmt("Failed to load preset <%1%>\n") % uri); + } + } else { + _status = Status::BAD_VALUE; + } + } + } + + if ((_graph = dynamic_cast<GraphImpl*>(_object))) { + if (key == uris.ingen_enabled) { + if (value.type() == uris.forge.Bool) { + op = SpecialType::ENABLE; + // FIXME: defer this until all other metadata has been processed + if (value.get<int32_t>() && !_graph->enabled()) { + if (!(_compiled_graph = compile(*_engine.maid(), *_graph))) { + _status = Status::COMPILATION_FAILED; + } + } + } else { + _status = Status::BAD_VALUE_TYPE; + } + } else if (key == uris.ingen_polyphony) { + if (value.type() == uris.forge.Int) { + if (value.get<int32_t>() < 1 || value.get<int32_t>() > 128) { + _status = Status::INVALID_POLY; + } else { + op = SpecialType::POLYPHONY; + _graph->prepare_internal_poly( + *_engine.buffer_factory(), value.get<int32_t>()); + } + } else { + _status = Status::BAD_VALUE_TYPE; + } + } + } + + if (!_create_event && key == uris.ingen_polyphonic) { + GraphImpl* parent = dynamic_cast<GraphImpl*>(obj->parent()); + if (!parent) { + _status = Status::BAD_OBJECT_TYPE; + } else if (value.type() != uris.forge.Bool) { + _status = Status::BAD_VALUE_TYPE; + } else { + op = SpecialType::POLYPHONIC; + obj->set_property(key, value, value.context()); + BlockImpl* block = dynamic_cast<BlockImpl*>(obj); + if (block) { + block->set_polyphonic(value.get<int32_t>()); + } + if (value.get<int32_t>()) { + obj->prepare_poly(*_engine.buffer_factory(), parent->internal_poly()); + } else { + obj->prepare_poly(*_engine.buffer_factory(), 1); + } + } + } + } else if (is_client && key == uris.ingen_broadcast) { + _engine.broadcaster()->set_broadcast( + _request_client, value.get<int32_t>()); + } else if (is_engine && key == uris.ingen_loadedBundle) { + LilvWorld* lworld = _engine.world()->lilv_world(); + LilvNode* bundle = get_file_node(lworld, uris, value); + if (bundle) { + lilv_world_load_bundle(lworld, bundle); + const std::set<PluginImpl*> new_plugins = + _engine.block_factory()->refresh(); + + for (PluginImpl* p : new_plugins) { + if (p->bundle_uri() == lilv_node_as_string(bundle)) { + _update.put_plugin(p); + } + } + lilv_node_free(bundle); + } else { + _status = Status::BAD_VALUE; + } + } + + if (_status != Status::NOT_PREPARED) { + break; + } + + _types.push_back(op); + } + + for (auto& s : _set_events) { + s->pre_process(ctx); + } + + return Event::pre_process_done( + _status == Status::NOT_PREPARED ? Status::SUCCESS : _status, + _subject); +} + +void +Delta::execute(RunContext& context) +{ + if (_status != Status::SUCCESS || _preset) { + return; + } + + const Ingen::URIs& uris = _engine.world()->uris(); + + if (_create_event) { + _create_event->set_time(_time); + _create_event->execute(context); + } + + for (auto& s : _set_events) { + s->set_time(_time); + s->execute(context); + } + + if (!_removed_bindings.empty()) { + _engine.control_bindings()->remove(context, _removed_bindings); + } + + NodeImpl* const object = dynamic_cast<NodeImpl*>(_object); + BlockImpl* const block = dynamic_cast<BlockImpl*>(_object); + PortImpl* const port = dynamic_cast<PortImpl*>(_object); + + std::vector<SpecialType>::const_iterator t = _types.begin(); + for (const auto& p : _properties) { + const URI& key = p.first; + const Atom& value = p.second; + switch (*t++) { + case SpecialType::ENABLE_BROADCAST: + if (port) { + port->enable_monitoring(value.get<int32_t>()); + } + break; + case SpecialType::ENABLE: + if (_graph) { + if (value.get<int32_t>()) { + if (_compiled_graph) { + _graph->set_compiled_graph(std::move(_compiled_graph)); + } + _graph->enable(); + } else { + _graph->disable(context); + } + } else if (block) { + block->set_enabled(value.get<int32_t>()); + } + break; + case SpecialType::POLYPHONIC: { + GraphImpl* parent = reinterpret_cast<GraphImpl*>(object->parent()); + if (value.get<int32_t>()) { + object->apply_poly(context, parent->internal_poly_process()); + } else { + object->apply_poly(context, 1); + } + } break; + case SpecialType::POLYPHONY: + if (!_graph->apply_internal_poly(context, + *_engine.buffer_factory(), + *_engine.maid(), + value.get<int32_t>())) { + _status = Status::INTERNAL_ERROR; + } + break; + case SpecialType::PORT_INDEX: + if (port) { + port->set_index(context, value.get<int32_t>()); + } + break; + case SpecialType::CONTROL_BINDING: + if (port) { + if (!_engine.control_bindings()->set_port_binding(context, port, _binding, value)) { + _status = Status::BAD_VALUE; + } + } else if (block) { + if (uris.ingen_Internal == block->plugin_impl()->type()) { + block->learn(); + } + } + break; + case SpecialType::PRESET: + block->set_enabled(false); + break; + case SpecialType::NONE: + if (port) { + if (key == uris.lv2_minimum) { + port->set_minimum(value); + } else if (key == uris.lv2_maximum) { + port->set_maximum(value); + } + } + case SpecialType::LOADED_BUNDLE: + break; + } + } +} + +void +Delta::post_process() +{ + if (_state) { + BlockImpl* block = dynamic_cast<BlockImpl*>(_object); + if (block) { + block->apply_state(_engine.sync_worker(), _state); + block->set_enabled(true); + } + lilv_state_free(_state); + } + + Broadcaster::Transfer t(*_engine.broadcaster()); + + if (_create_event) { + _create_event->post_process(); + if (_create_event->status() != Status::SUCCESS) { + return; // Creation failed, nothing else to do + } + } + + for (auto& s : _set_events) { + if (s->synthetic() || s->status() != Status::SUCCESS) { + s->post_process(); // Set failed, report error + } + } + + if (respond() == Status::SUCCESS) { + _update.send(*_engine.broadcaster()); + + switch (_type) { + case Type::SET: + /* Kludge to avoid feedback for set events only. The GUI + depends on put responses to e.g. initially place blocks. + Some more sensible way of controlling this is needed. */ + if (_mode == Mode::NORMAL) { + _engine.broadcaster()->set_ignore_client(_request_client); + } + _engine.broadcaster()->set_property( + _subject, + _properties.begin()->first, + _properties.begin()->second); + if (_mode == Mode::NORMAL) { + _engine.broadcaster()->clear_ignore_client(); + } + break; + case Type::PUT: + if (_type == Type::PUT && _subject.scheme() == "file") { + // Preset save + ClientUpdate response; + response.put(_preset->uri(), _preset->properties()); + response.send(*_engine.broadcaster()); + } else { + // Graph object put + _engine.broadcaster()->put(_subject, _properties, _context); + } + break; + case Type::PATCH: + _engine.broadcaster()->delta(_subject, _remove, _properties, _context); + break; + } + } +} + +void +Delta::undo(Interface& target) +{ + if (_create_event) { + _create_event->undo(target); + } else if (_type == Type::PATCH) { + target.delta(_subject, _added, _removed, _context); + } else if (_type == Type::SET || _type == Type::PUT) { + if (_removed.size() == 1) { + target.set_property(_subject, + _removed.begin()->first, + _removed.begin()->second, + _context); + } else if (_removed.empty()) { + target.delta(_subject, _added, {}, _context); + } else { + target.put(_subject, _removed, _context); + } + } +} + +Event::Execution +Delta::get_execution() const +{ + return _block ? Execution::ATOMIC : Execution::NORMAL; +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Delta.hpp b/src/server/events/Delta.hpp new file mode 100644 index 00000000..af337b57 --- /dev/null +++ b/src/server/events/Delta.hpp @@ -0,0 +1,133 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_DELTA_HPP +#define INGEN_EVENTS_DELTA_HPP + +#include <vector> + +#include <boost/optional.hpp> + +#include "lilv/lilv.h" + +#include "CompiledGraph.hpp" +#include "ControlBindings.hpp" +#include "Event.hpp" +#include "PluginImpl.hpp" + +namespace Ingen { + +class Resource; + +namespace Server { + +class Engine; +class GraphImpl; +class RunContext; + +namespace Events { + +class SetPortValue; + +/** Set properties of a graph object. + * \ingroup engine + */ +class Delta : public Event +{ +public: + Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Put& msg); + + Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Delta& msg); + + Delta(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::SetProperty& msg); + + ~Delta(); + + void add_set_event(const char* port_symbol, + const void* value, + uint32_t size, + uint32_t type); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + + Execution get_execution() const; + +private: + enum class Type { + SET, + PUT, + PATCH + }; + + enum class SpecialType { + NONE, + ENABLE, + ENABLE_BROADCAST, + POLYPHONY, + POLYPHONIC, + PORT_INDEX, + CONTROL_BINDING, + PRESET, + LOADED_BUNDLE + }; + + typedef std::vector<SetPortValue*> SetEvents; + + void init(); + + Event* _create_event; + SetEvents _set_events; + std::vector<SpecialType> _types; + std::vector<SpecialType> _remove_types; + URI _subject; + Properties _properties; + Properties _remove; + ClientUpdate _update; + Ingen::Resource* _object; + GraphImpl* _graph; + MPtr<CompiledGraph> _compiled_graph; + ControlBindings::Binding* _binding; + LilvState* _state; + Resource::Graph _context; + Type _type; + + Properties _added; + Properties _removed; + + std::vector<ControlBindings::Binding*> _removed_bindings; + + boost::optional<Resource> _preset; + + bool _block; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_DELTA_HPP diff --git a/src/server/events/Disconnect.cpp b/src/server/events/Disconnect.cpp new file mode 100644 index 00000000..4553c8a2 --- /dev/null +++ b/src/server/events/Disconnect.cpp @@ -0,0 +1,224 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <set> + +#include "ingen/Store.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "ArcImpl.hpp" +#include "Broadcaster.hpp" +#include "Buffer.hpp" +#include "DuplexPort.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" +#include "events/Disconnect.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Disconnect::Disconnect(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Disconnect& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _graph(nullptr) + , _impl(nullptr) +{ +} + +Disconnect::~Disconnect() +{ + delete _impl; +} + +Disconnect::Impl::Impl(Engine& e, + GraphImpl* graph, + PortImpl* t, + InputPort* h) + : _engine(e) + , _tail(t) + , _head(h) + , _arc(graph->remove_arc(_tail, _head)) +{ + ThreadManager::assert_thread(THREAD_PRE_PROCESS); + + BlockImpl* const tail_block = _tail->parent_block(); + BlockImpl* const head_block = _head->parent_block(); + + // Remove tail from head's providers + auto hp = head_block->providers().find(tail_block); + if (hp != head_block->providers().end()) { + head_block->providers().erase(hp); + } + + // Remove head from tail's providers + auto td = tail_block->dependants().find(head_block); + if (td != tail_block->dependants().end()) { + tail_block->dependants().erase(td); + } + + _head->decrement_num_arcs(); + + if (_head->num_arcs() == 0) { + if (!_head->is_driver_port()) { + BufferFactory& bufs = *_engine.buffer_factory(); + _voices = bufs.maid().make_managed<PortImpl::Voices>(_head->poly()); + _head->pre_get_buffers(bufs, _voices, _head->poly()); + + if (_head->is_a(PortType::CONTROL) || + _head->is_a(PortType::CV)) { + // Reset buffer to control value + const float value = _head->value().get<float>(); + for (uint32_t i = 0; i < _voices->size(); ++i) { + Buffer* buf = _voices->at(i).buffer.get(); + buf->set_block(value, 0, e.block_length()); + } + } else { + for (uint32_t i = 0; i < _voices->size(); ++i) { + _voices->at(i).buffer->clear(); + } + } + } + } +} + +bool +Disconnect::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + if (_msg.tail.parent().parent() != _msg.head.parent().parent() + && _msg.tail.parent() != _msg.head.parent().parent() + && _msg.tail.parent().parent() != _msg.head.parent()) { + return Event::pre_process_done(Status::PARENT_DIFFERS, _msg.head); + } + + PortImpl* tail = dynamic_cast<PortImpl*>(_engine.store()->get(_msg.tail)); + if (!tail) { + return Event::pre_process_done(Status::PORT_NOT_FOUND, _msg.tail); + } + + PortImpl* head = dynamic_cast<PortImpl*>(_engine.store()->get(_msg.head)); + if (!head) { + return Event::pre_process_done(Status::PORT_NOT_FOUND, _msg.head); + } + + BlockImpl* const tail_block = tail->parent_block(); + BlockImpl* const head_block = head->parent_block(); + + if (tail_block->parent_graph() != head_block->parent_graph()) { + // Arc to a graph port from inside the graph + assert(tail_block->parent() == head_block || head_block->parent() == tail_block); + if (tail_block->parent() == head_block) { + _graph = dynamic_cast<GraphImpl*>(head_block); + } else { + _graph = dynamic_cast<GraphImpl*>(tail_block); + } + } else if (tail_block == head_block && dynamic_cast<GraphImpl*>(tail_block)) { + // Arc from a graph input to a graph output (pass through) + _graph = dynamic_cast<GraphImpl*>(tail_block); + } else { + // Normal arc between blocks with the same parent + _graph = tail_block->parent_graph(); + } + + if (!_graph) { + return Event::pre_process_done(Status::INTERNAL_ERROR, _msg.head); + } else if (!_graph->has_arc(tail, head)) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.head); + } + + if (tail_block == nullptr || head_block == nullptr) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, _msg.head); + } + + _impl = new Impl(_engine, + _graph, + dynamic_cast<PortImpl*>(tail), + dynamic_cast<InputPort*>(head)); + + _compiled_graph = ctx.maybe_compile(*_engine.maid(), *_graph); + + return Event::pre_process_done(Status::SUCCESS); +} + +bool +Disconnect::Impl::execute(RunContext& context, bool set_head_buffers) +{ + if (!_arc) { + return false; + } + + _head->remove_arc(*_arc.get()); + if (_head->is_driver_port()) { + return true; + } + + if (set_head_buffers) { + if (_voices) { + _head->set_voices(context, std::move(_voices)); + } else { + _head->setup_buffers(context, *_engine.buffer_factory(), _head->poly()); + } + _head->connect_buffers(); + } else { + _head->recycle_buffers(); + } + + return true; +} + +void +Disconnect::execute(RunContext& context) +{ + if (_status == Status::SUCCESS) { + if (_impl->execute(context, true)) { + if (_compiled_graph) { + _graph->set_compiled_graph(std::move(_compiled_graph)); + } + } else { + _status = Status::NOT_FOUND; + } + } +} + +void +Disconnect::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + } +} + +void +Disconnect::undo(Interface& target) +{ + target.connect(_msg.tail, _msg.head); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Disconnect.hpp b/src/server/events/Disconnect.hpp new file mode 100644 index 00000000..44290d7c --- /dev/null +++ b/src/server/events/Disconnect.hpp @@ -0,0 +1,87 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_DISCONNECT_HPP +#define INGEN_EVENTS_DISCONNECT_HPP + +#include "raul/Path.hpp" + +#include "BufferFactory.hpp" +#include "CompiledGraph.hpp" +#include "Event.hpp" +#include "GraphImpl.hpp" +#include "types.hpp" + +namespace Raul { +template <typename T> class Array; +} + +namespace Ingen { +namespace Server { + +class InputPort; +class PortImpl; + +namespace Events { + +/** Remove an Arc between two Ports. + * + * \ingroup engine + */ +class Disconnect : public Event +{ +public: + Disconnect(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Disconnect& msg); + + ~Disconnect(); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + + class Impl { + public: + Impl(Engine& e, GraphImpl* graph, PortImpl* t, InputPort* h); + + bool execute(RunContext& context, bool set_head_buffers); + + inline PortImpl* tail() { return _tail; } + inline InputPort* head() { return _head; } + + private: + Engine& _engine; + PortImpl* _tail; + InputPort* _head; + SPtr<ArcImpl> _arc; + MPtr<PortImpl::Voices> _voices; + }; + +private: + const Ingen::Disconnect _msg; + GraphImpl* _graph; + Impl* _impl; + MPtr<CompiledGraph> _compiled_graph; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_DISCONNECT_HPP diff --git a/src/server/events/DisconnectAll.cpp b/src/server/events/DisconnectAll.cpp new file mode 100644 index 00000000..11311d12 --- /dev/null +++ b/src/server/events/DisconnectAll.cpp @@ -0,0 +1,176 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <set> + +#include "ingen/Store.hpp" +#include "raul/Array.hpp" +#include "raul/Maid.hpp" +#include "raul/Path.hpp" + +#include "ArcImpl.hpp" +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "Engine.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "PortImpl.hpp" +#include "PreProcessContext.hpp" +#include "events/Disconnect.hpp" +#include "events/DisconnectAll.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +DisconnectAll::DisconnectAll(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::DisconnectAll& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _parent(nullptr) + , _block(nullptr) + , _port(nullptr) + , _deleting(false) +{ +} + +/** Internal version for use by other events. + */ +DisconnectAll::DisconnectAll(Engine& engine, + GraphImpl* parent, + Node* object) + : Event(engine) + , _msg{0, parent->path(), object->path()} + , _parent(parent) + , _block(dynamic_cast<BlockImpl*>(object)) + , _port(dynamic_cast<PortImpl*>(object)) + , _deleting(true) +{ +} + +DisconnectAll::~DisconnectAll() +{ + for (auto& i : _impls) { + delete i; + } +} + +bool +DisconnectAll::pre_process(PreProcessContext& ctx) +{ + std::unique_lock<Store::Mutex> lock(_engine.store()->mutex(), + std::defer_lock); + + if (!_deleting) { + lock.lock(); + + _parent = dynamic_cast<GraphImpl*>(_engine.store()->get(_msg.graph)); + if (!_parent) { + return Event::pre_process_done(Status::PARENT_NOT_FOUND, + _msg.graph); + } + + NodeImpl* const object = dynamic_cast<NodeImpl*>( + _engine.store()->get(_msg.path)); + if (!object) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.path); + } + + if (object->parent_graph() != _parent + && object->parent()->parent_graph() != _parent) { + return Event::pre_process_done(Status::INVALID_PARENT, _msg.graph); + } + + // Only one of these will succeed + _block = dynamic_cast<BlockImpl*>(object); + _port = dynamic_cast<PortImpl*>(object); + + if (!_block && !_port) { + return Event::pre_process_done(Status::INTERNAL_ERROR, _msg.path); + } + } + + // Find set of arcs to remove + std::set<ArcImpl*> to_remove; + for (const auto& a : _parent->arcs()) { + ArcImpl* const arc = (ArcImpl*)a.second.get(); + if (_block) { + if (arc->tail()->parent_block() == _block + || arc->head()->parent_block() == _block) { + to_remove.insert(arc); + } + } else if (_port) { + if (arc->tail() == _port || arc->head() == _port) { + to_remove.insert(arc); + } + } + } + + // Create disconnect events (which erases from _parent->arcs()) + for (const auto& a : to_remove) { + _impls.push_back(new Disconnect::Impl( + _engine, _parent, + dynamic_cast<PortImpl*>(a->tail()), + dynamic_cast<InputPort*>(a->head()))); + } + + if (!_deleting && ctx.must_compile(*_parent)) { + if (!(_compiled_graph = compile(*_engine.maid(), *_parent))) { + return Event::pre_process_done(Status::COMPILATION_FAILED); + } + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +DisconnectAll::execute(RunContext& context) +{ + if (_status == Status::SUCCESS) { + for (auto& i : _impls) { + i->execute(context, + !_deleting || (i->head()->parent_block() != _block)); + } + } + + if (_compiled_graph) { + _parent->set_compiled_graph(std::move(_compiled_graph)); + } +} + +void +DisconnectAll::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + } +} + +void +DisconnectAll::undo(Interface& target) +{ + for (auto& i : _impls) { + target.connect(i->tail()->path(), i->head()->path()); + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/DisconnectAll.hpp b/src/server/events/DisconnectAll.hpp new file mode 100644 index 00000000..947e538f --- /dev/null +++ b/src/server/events/DisconnectAll.hpp @@ -0,0 +1,78 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_DISCONNECTALL_HPP +#define INGEN_EVENTS_DISCONNECTALL_HPP + +#include <list> + +#include "raul/Path.hpp" + +#include "CompiledGraph.hpp" +#include "Disconnect.hpp" +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; +class PortImpl; + +namespace Events { + +class Disconnect; + +/** An event to disconnect all connections to a Block. + * + * \ingroup engine + */ +class DisconnectAll : public Event +{ +public: + DisconnectAll(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::DisconnectAll& msg); + + DisconnectAll(Engine& engine, + GraphImpl* parent, + Node* object); + + ~DisconnectAll(); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + typedef std::list<Disconnect::Impl*> Impls; + + const Ingen::DisconnectAll _msg; + GraphImpl* _parent; + BlockImpl* _block; + PortImpl* _port; + Impls _impls; + MPtr<CompiledGraph> _compiled_graph; + bool _deleting; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_DISCONNECTALL_HPP diff --git a/src/server/events/Get.cpp b/src/server/events/Get.cpp new file mode 100644 index 00000000..e53e8c41 --- /dev/null +++ b/src/server/events/Get.cpp @@ -0,0 +1,111 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <utility> + +#include "ingen/Interface.hpp" +#include "ingen/Node.hpp" +#include "ingen/Store.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "BufferFactory.hpp" +#include "Engine.hpp" +#include "Get.hpp" +#include "GraphImpl.hpp" +#include "PluginImpl.hpp" +#include "PortImpl.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Get::Get(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Get& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) + , _object(nullptr) + , _plugin(nullptr) +{} + +bool +Get::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + const auto& uri = _msg.subject; + if (uri == "ingen:/plugins") { + _plugins = _engine.block_factory()->plugins(); + return Event::pre_process_done(Status::SUCCESS); + } else if (uri == "ingen:/engine") { + return Event::pre_process_done(Status::SUCCESS); + } else if (uri_is_path(uri)) { + if ((_object = _engine.store()->get(uri_to_path(uri)))) { + const BlockImpl* block = nullptr; + const GraphImpl* graph = nullptr; + const PortImpl* port = nullptr; + if ((graph = dynamic_cast<const GraphImpl*>(_object))) { + _response.put_graph(graph); + } else if ((block = dynamic_cast<const BlockImpl*>(_object))) { + _response.put_block(block); + } else if ((port = dynamic_cast<const PortImpl*>(_object))) { + _response.put_port(port); + } else { + return Event::pre_process_done(Status::BAD_OBJECT_TYPE, uri); + } + return Event::pre_process_done(Status::SUCCESS); + } + return Event::pre_process_done(Status::NOT_FOUND, uri); + } else if ((_plugin = _engine.block_factory()->plugin(uri))) { + _response.put_plugin(_plugin); + return Event::pre_process_done(Status::SUCCESS); + } else { + return Event::pre_process_done(Status::NOT_FOUND, uri); + } +} + +void +Get::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS && _request_client) { + if (_msg.subject == "ingen:/plugins") { + _engine.broadcaster()->send_plugins_to(_request_client.get(), _plugins); + } else if (_msg.subject == "ingen:/engine") { + // TODO: Keep a proper RDF model of the engine + URIs& uris = _engine.world()->uris(); + Properties props = { + { uris.param_sampleRate, + uris.forge.make(int32_t(_engine.sample_rate())) }, + { uris.bufsz_maxBlockLength, + uris.forge.make(int32_t(_engine.block_length())) }, + { uris.ingen_numThreads, + uris.forge.make(int32_t(_engine.n_threads())) } }; + + const Properties load_props = _engine.load_properties(); + props.insert(load_props.begin(), load_props.end()); + _request_client->put(URI("ingen:/engine"), props); + } else { + _response.send(*_request_client); + } + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Get.hpp b/src/server/events/Get.hpp new file mode 100644 index 00000000..7392550f --- /dev/null +++ b/src/server/events/Get.hpp @@ -0,0 +1,65 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_GET_HPP +#define INGEN_EVENTS_GET_HPP + +#include <vector> + +#include "BlockFactory.hpp" +#include "ClientUpdate.hpp" +#include "Event.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class BlockImpl; +class GraphImpl; +class PluginImpl; +class PortImpl; + +namespace Events { + +/** A request from a client to send an object. + * + * \ingroup engine + */ +class Get : public Event +{ +public: + Get(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Get& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context) {} + void post_process(); + +private: + const Ingen::Get _msg; + const Node* _object; + PluginImpl* _plugin; + BlockFactory::Plugins _plugins; + ClientUpdate _response; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_GET_HPP diff --git a/src/server/events/Mark.cpp b/src/server/events/Mark.cpp new file mode 100644 index 00000000..3c0dfaaf --- /dev/null +++ b/src/server/events/Mark.cpp @@ -0,0 +1,112 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "Engine.hpp" +#include "PreProcessContext.hpp" +#include "UndoStack.hpp" +#include "events/Mark.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Mark::Mark(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::BundleBegin& msg) + : Event(engine, client, msg.seq, timestamp) + , _type(Type::BUNDLE_BEGIN) + , _depth(0) +{} + +Mark::Mark(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::BundleEnd& msg) + : Event(engine, client, msg.seq, timestamp) + , _type(Type::BUNDLE_END) + , _depth(0) +{} + +bool +Mark::pre_process(PreProcessContext& ctx) +{ + const UPtr<UndoStack>& stack = ((_mode == Mode::UNDO) + ? _engine.redo_stack() + : _engine.undo_stack()); + + switch (_type) { + case Type::BUNDLE_BEGIN: + ctx.set_in_bundle(true); + _depth = stack->start_entry(); + break; + case Type::BUNDLE_END: + _depth = stack->finish_entry(); + ctx.set_in_bundle(false); + if (!ctx.dirty_graphs().empty()) { + for (GraphImpl* g : ctx.dirty_graphs()) { + MPtr<CompiledGraph> cg = compile(*_engine.maid(), *g); + if (cg) { + _compiled_graphs.emplace(g, std::move(cg)); + } + } + ctx.dirty_graphs().clear(); + } + break; + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Mark::execute(RunContext& context) +{ + for (auto& g : _compiled_graphs) { + g.first->set_compiled_graph(std::move(g.second)); + } +} + +void +Mark::post_process() +{ + respond(); +} + +Event::Execution +Mark::get_execution() const +{ + if (!_engine.atomic_bundles()) { + return Execution::NORMAL; + } + + switch (_type) { + case Type::BUNDLE_BEGIN: + if (_depth == 1) { + return Execution::BLOCK; + } + break; + case Type::BUNDLE_END: + if (_depth == 0) { + return Execution::UNBLOCK; + } + break; + } + return Execution::NORMAL; +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Mark.hpp b/src/server/events/Mark.hpp new file mode 100644 index 00000000..eaeb9332 --- /dev/null +++ b/src/server/events/Mark.hpp @@ -0,0 +1,69 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_MARK_HPP +#define INGEN_EVENTS_MARK_HPP + +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class Engine; + +namespace Events { + +/** Delineate the start or end of a bundle of events. + * + * This is used to mark the ends of an undo transaction, so a single undo can + * undo the effects of many events (such as a paste or a graph load). + * + * \ingroup engine + */ +class Mark : public Event +{ +public: + Mark(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::BundleBegin& msg); + + Mark(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::BundleEnd& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + + Execution get_execution() const; + +private: + enum class Type { BUNDLE_BEGIN, BUNDLE_END }; + + typedef std::map<GraphImpl*, MPtr<CompiledGraph>> CompiledGraphs; + + CompiledGraphs _compiled_graphs; + Type _type; + int _depth; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_MARK_HPP diff --git a/src/server/events/Move.cpp b/src/server/events/Move.cpp new file mode 100644 index 00000000..b0935675 --- /dev/null +++ b/src/server/events/Move.cpp @@ -0,0 +1,91 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Store.hpp" +#include "raul/Path.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "EnginePort.hpp" +#include "GraphImpl.hpp" +#include "events/Move.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Move::Move(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Move& msg) + : Event(engine, client, msg.seq, timestamp) + , _msg(msg) +{ +} + +bool +Move::pre_process(PreProcessContext& ctx) +{ + std::lock_guard<Store::Mutex> lock(_engine.store()->mutex()); + + if (!_msg.old_path.parent().is_parent_of(_msg.new_path)) { + return Event::pre_process_done(Status::PARENT_DIFFERS, _msg.new_path); + } + + const Store::iterator i = _engine.store()->find(_msg.old_path); + if (i == _engine.store()->end()) { + return Event::pre_process_done(Status::NOT_FOUND, _msg.old_path); + } + + if (_engine.store()->find(_msg.new_path) != _engine.store()->end()) { + return Event::pre_process_done(Status::EXISTS, _msg.new_path); + } + + EnginePort* eport = _engine.driver()->get_port(_msg.old_path); + if (eport) { + _engine.driver()->rename_port(_msg.old_path, _msg.new_path); + } + + _engine.store()->rename(i, _msg.new_path); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Move::execute(RunContext& context) +{ +} + +void +Move::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS) { + _engine.broadcaster()->message(_msg); + } +} + +void +Move::undo(Interface& target) +{ + target.move(_msg.new_path, _msg.old_path); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Move.hpp b/src/server/events/Move.hpp new file mode 100644 index 00000000..459d2709 --- /dev/null +++ b/src/server/events/Move.hpp @@ -0,0 +1,57 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_MOVE_HPP +#define INGEN_EVENTS_MOVE_HPP + +#include "ingen/Store.hpp" +#include "raul/Path.hpp" + +#include "Event.hpp" + +namespace Ingen { +namespace Server { + +class GraphImpl; +class PortImpl; + +namespace Events { + +/** Move a graph object to a new path. + * \ingroup engine + */ +class Move : public Event +{ +public: + Move(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Move& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + void undo(Interface& target); + +private: + const Ingen::Move _msg; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_MOVE_HPP diff --git a/src/server/events/SetPortValue.cpp b/src/server/events/SetPortValue.cpp new file mode 100644 index 00000000..62f2def6 --- /dev/null +++ b/src/server/events/SetPortValue.cpp @@ -0,0 +1,139 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/LV2Features.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIs.hpp" +#include "ingen/World.hpp" + +#include "BlockImpl.hpp" +#include "Broadcaster.hpp" +#include "Buffer.hpp" +#include "ControlBindings.hpp" +#include "Engine.hpp" +#include "PortImpl.hpp" +#include "RunContext.hpp" +#include "SetPortValue.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +/** Internal */ +SetPortValue::SetPortValue(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + PortImpl* port, + const Atom& value, + bool activity, + bool synthetic) + : Event(engine, client, id, timestamp) + , _port(port) + , _value(value) + , _activity(activity) + , _synthetic(synthetic) +{ +} + +bool +SetPortValue::pre_process(PreProcessContext& ctx) +{ + Ingen::URIs& uris = _engine.world()->uris(); + if (_port->is_output()) { + return Event::pre_process_done(Status::DIRECTION_MISMATCH, _port->path()); + } + + if (!_activity) { + // Set value metadata (does not affect buffers) + _port->set_value(_value); + _port->set_property(_engine.world()->uris().ingen_value, _value); + } + + _binding = _engine.control_bindings()->port_binding(_port); + + if (_port->buffer_type() == uris.atom_Sequence) { + _buffer = _engine.buffer_factory()->get_buffer( + _port->buffer_type(), + _value.type() == uris.atom_Float ? _value.type() : 0, + _engine.buffer_factory()->default_size(_port->buffer_type())); + } + + return Event::pre_process_done(Status::SUCCESS); +} + +void +SetPortValue::execute(RunContext& context) +{ + assert(_time >= context.start() && _time <= context.end()); + apply(context); + _engine.control_bindings()->port_value_changed(context, _port, _binding, _value); +} + +void +SetPortValue::apply(RunContext& context) +{ + if (_status != Status::SUCCESS) { + return; + } + + Ingen::URIs& uris = _engine.world()->uris(); + Buffer* buf = _port->buffer(0).get(); + + if (_buffer) { + if (_port->user_buffer(context)) { + buf = _port->user_buffer(context).get(); + } else { + _port->set_user_buffer(context, _buffer); + buf = _buffer.get(); + } + } + + if (buf->type() == uris.atom_Sound || buf->type() == uris.atom_Float) { + if (_value.type() == uris.forge.Float) { + _port->set_control_value(context, _time, _value.get<float>()); + } else { + _status = Status::TYPE_MISMATCH; + } + } else if (buf->type() == uris.atom_Sequence) { + if (!buf->append_event(_time - context.start(), + _value.size(), + _value.type(), + (const uint8_t*)_value.get_body())) { + _status = Status::NO_SPACE; + } + } else if (buf->type() == uris.atom_URID) { + buf->get<LV2_Atom_URID>()->body = _value.get<int32_t>(); + } else { + _status = Status::BAD_VALUE_TYPE; + } +} + +void +SetPortValue::post_process() +{ + Broadcaster::Transfer t(*_engine.broadcaster()); + if (respond() == Status::SUCCESS && !_activity) { + _engine.broadcaster()->set_property( + _port->uri(), + _engine.world()->uris().ingen_value, + _value); + } +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/SetPortValue.hpp b/src/server/events/SetPortValue.hpp new file mode 100644 index 00000000..4df60019 --- /dev/null +++ b/src/server/events/SetPortValue.hpp @@ -0,0 +1,71 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_SETPORTVALUE_HPP +#define INGEN_EVENTS_SETPORTVALUE_HPP + +#include "ingen/Atom.hpp" + +#include "BufferRef.hpp" +#include "ControlBindings.hpp" +#include "Event.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class PortImpl; + +namespace Events { + +/** An event to change the value of a port. + * + * \ingroup engine + */ +class SetPortValue : public Event +{ +public: + SetPortValue(Engine& engine, + SPtr<Interface> client, + int32_t id, + SampleCount timestamp, + PortImpl* port, + const Atom& value, + bool activity, + bool synthetic = false); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + + bool synthetic() const { return _synthetic; } + +private: + void apply(RunContext& context); + + PortImpl* _port; + const Atom _value; + BufferRef _buffer; + ControlBindings::Key _binding; + bool _activity; + bool _synthetic; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_SETPORTVALUE_HPP diff --git a/src/server/events/Undo.cpp b/src/server/events/Undo.cpp new file mode 100644 index 00000000..e06a5951 --- /dev/null +++ b/src/server/events/Undo.cpp @@ -0,0 +1,85 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/AtomReader.hpp" + +#include "Engine.hpp" +#include "EventWriter.hpp" +#include "Undo.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +Undo::Undo(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Undo& msg) + : Event(engine, client, msg.seq, timestamp) + , _is_redo(false) +{} + +Undo::Undo(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Redo& msg) + : Event(engine, client, msg.seq, timestamp) + , _is_redo(true) +{} + +bool +Undo::pre_process(PreProcessContext& ctx) +{ + const UPtr<UndoStack>& stack = _is_redo ? _engine.redo_stack() : _engine.undo_stack(); + const Event::Mode mode = _is_redo ? Event::Mode::REDO : Event::Mode::UNDO; + + if (stack->empty()) { + return Event::pre_process_done(Status::NOT_FOUND); + } + + const Event::Mode orig_mode = _engine.event_writer()->get_event_mode(); + _entry = stack->pop(); + _engine.event_writer()->set_event_mode(mode); + if (_entry.events.size() > 1) { + _engine.interface()->bundle_begin(); + } + + for (const LV2_Atom* ev : _entry.events) { + _engine.atom_interface()->write(ev); + } + + if (_entry.events.size() > 1) { + _engine.interface()->bundle_end(); + } + _engine.event_writer()->set_event_mode(orig_mode); + + return Event::pre_process_done(Status::SUCCESS); +} + +void +Undo::execute(RunContext& context) +{ +} + +void +Undo::post_process() +{ + respond(); +} + +} // namespace Events +} // namespace Server +} // namespace Ingen diff --git a/src/server/events/Undo.hpp b/src/server/events/Undo.hpp new file mode 100644 index 00000000..af4b0d65 --- /dev/null +++ b/src/server/events/Undo.hpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_EVENTS_UNDO_HPP +#define INGEN_EVENTS_UNDO_HPP + +#include "Event.hpp" +#include "UndoStack.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { +namespace Events { + +/** A request to undo the last change to the engine. + * + * \ingroup engine + */ +class Undo : public Event +{ +public: + Undo(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Undo& msg); + + Undo(Engine& engine, + SPtr<Interface> client, + SampleCount timestamp, + const Ingen::Redo& msg); + + bool pre_process(PreProcessContext& ctx); + void execute(RunContext& context); + void post_process(); + +private: + UndoStack::Entry _entry; + bool _is_redo; +}; + +} // namespace Events +} // namespace Server +} // namespace Ingen + +#endif // INGEN_EVENTS_UNDO_HPP diff --git a/src/server/ingen_engine.cpp b/src/server/ingen_engine.cpp new file mode 100644 index 00000000..3409f1bf --- /dev/null +++ b/src/server/ingen_engine.cpp @@ -0,0 +1,44 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/Module.hpp" +#include "ingen/World.hpp" +#include "Engine.hpp" +#include "EventWriter.hpp" +#include "util.hpp" + +using namespace Ingen; + +struct IngenEngineModule : public Ingen::Module { + virtual void load(Ingen::World* world) { + Server::set_denormal_flags(world->log()); + SPtr<Server::Engine> engine(new Server::Engine(world)); + world->set_engine(engine); + if (!world->interface()) { + world->set_interface(engine->interface()); + } + } +}; + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + return new IngenEngineModule(); +} + +} // extern "C" diff --git a/src/server/ingen_jack.cpp b/src/server/ingen_jack.cpp new file mode 100644 index 00000000..a897f130 --- /dev/null +++ b/src/server/ingen_jack.cpp @@ -0,0 +1,58 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/Atom.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/World.hpp" + +#include "JackDriver.hpp" +#include "Engine.hpp" + +using namespace Ingen; + +struct IngenJackModule : public Ingen::Module { + void load(Ingen::World* world) { + if (((Server::Engine*)world->engine().get())->driver()) { + world->log().warn("Engine already has a driver\n"); + return; + } + + Server::JackDriver* driver = new Server::JackDriver( + *(Server::Engine*)world->engine().get()); + const Atom& s = world->conf().option("jack-server"); + const std::string server_name = s.is_valid() ? s.ptr<char>() : ""; + driver->attach(server_name, + world->conf().option("jack-name").ptr<char>(), + nullptr); + ((Server::Engine*)world->engine().get())->set_driver( + SPtr<Server::Driver>(driver)); + } +}; + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + return new IngenJackModule(); +} + +} // extern "C" diff --git a/src/server/ingen_lv2.cpp b/src/server/ingen_lv2.cpp new file mode 100644 index 00000000..b2806ab6 --- /dev/null +++ b/src/server/ingen_lv2.cpp @@ -0,0 +1,850 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cstdlib> +#include <string> +#include <thread> +#include <vector> + +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" +#include "lv2/lv2plug.in/ns/ext/log/log.h" +#include "lv2/lv2plug.in/ns/ext/log/logger.h" +#include "lv2/lv2plug.in/ns/ext/options/options.h" +#include "lv2/lv2plug.in/ns/ext/state/state.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +#include "ingen/AtomReader.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" +#include "ingen/Parser.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/Store.hpp" +#include "ingen/URI.hpp" +#include "ingen/World.hpp" +#include "ingen/ingen.h" +#include "ingen/runtime_paths.hpp" +#include "ingen/types.hpp" +#include "raul/Semaphore.hpp" + +#include "Buffer.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "EnginePort.hpp" +#include "EventWriter.hpp" +#include "GraphImpl.hpp" +#include "PostProcessor.hpp" +#include "RunContext.hpp" +#include "ThreadManager.hpp" + +#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +#define NS_RDFS "http://www.w3.org/2000/01/rdf-schema#" + +namespace Ingen { + +/** Record of a graph in this bundle. */ +struct LV2Graph : public Parser::ResourceRecord { + LV2Graph(Parser::ResourceRecord record); + + LV2_Descriptor descriptor; +}; + +/** Ingen LV2 library. */ +class Lib { +public: + explicit Lib(const char* bundle_path); + + typedef std::vector< SPtr<const LV2Graph> > Graphs; + + Graphs graphs; +}; + +namespace Server { + +class LV2Driver; + +void signal_main(RunContext& context, LV2Driver* driver); + +inline size_t +ui_ring_size(SampleCount block_length) +{ + return std::max((size_t)8192, (size_t)block_length * 16); +} + +class LV2Driver : public Ingen::Server::Driver + , public Ingen::AtomSink +{ +public: + LV2Driver(Engine& engine, + SampleCount block_length, + size_t seq_size, + SampleCount sample_rate) + : _engine(engine) + , _main_sem(0) + , _reader(engine.world()->uri_map(), + engine.world()->uris(), + engine.world()->log(), + *engine.world()->interface().get()) + , _writer(engine.world()->uri_map(), + engine.world()->uris(), + *this) + , _from_ui(ui_ring_size(block_length)) + , _to_ui(ui_ring_size(block_length)) + , _root_graph(nullptr) + , _notify_capacity(0) + , _block_length(block_length) + , _seq_size(seq_size) + , _sample_rate(sample_rate) + , _frame_time(0) + , _to_ui_overflow_sem(0) + , _to_ui_overflow(false) + , _instantiated(false) + {} + + virtual bool dynamic_ports() const { return !_instantiated; } + + void pre_process_port(RunContext& context, EnginePort* port) { + const URIs& uris = _engine.world()->uris(); + const SampleCount nframes = context.nframes(); + DuplexPort* graph_port = port->graph_port(); + Buffer* graph_buf = graph_port->buffer(0).get(); + void* lv2_buf = port->buffer(); + + if (graph_port->is_a(PortType::AUDIO) || graph_port->is_a(PortType::CV)) { + graph_port->set_driver_buffer(lv2_buf, nframes * sizeof(float)); + } else if (graph_port->buffer_type() == uris.atom_Sequence) { + graph_port->set_driver_buffer(lv2_buf, lv2_atom_total_size((LV2_Atom*)lv2_buf)); + if (graph_port->symbol() == "control") { // TODO: Safe to use index? + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)lv2_buf; + bool enqueued = false; + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (AtomReader::is_message(uris, &ev->body)) { + enqueued = enqueue_message(&ev->body) || enqueued; + } + } + + if (enqueued) { + // Enqueued a message for processing, raise semaphore + _main_sem.post(); + } + } + } + + if (graph_port->is_input()) { + graph_port->monitor(context); + } else { + graph_buf->prepare_write(context); + } + } + + void post_process_port(RunContext& context, EnginePort* port) { + DuplexPort* graph_port = port->graph_port(); + + // No copying necessary, host buffers are used directly + // Reset graph port buffer pointer to no longer point to the Jack buffer + if (graph_port->is_driver_port()) { + graph_port->set_driver_buffer(nullptr, 0); + } + } + + void run(uint32_t nframes) { + _engine.locate(_frame_time, nframes); + + // Notify buffer is a Chunk with size set to the available space + _notify_capacity = ((LV2_Atom_Sequence*)_ports[1]->buffer())->atom.size; + + for (auto& p : _ports) { + pre_process_port(_engine.run_context(), p); + } + + _engine.run(nframes); + if (_engine.post_processor()->pending()) { + _main_sem.post(); + } + + flush_to_ui(_engine.run_context()); + + for (auto& p : _ports) { + post_process_port(_engine.run_context(), p); + } + + _frame_time += nframes; + } + + virtual void deactivate() { + _engine.quit(); + _main_sem.post(); + } + + virtual void set_root_graph(GraphImpl* graph) { _root_graph = graph; } + virtual GraphImpl* root_graph() { return _root_graph; } + + virtual EnginePort* get_port(const Raul::Path& path) { + for (auto& p : _ports) { + if (p->graph_port()->path() == path) { + return p; + } + } + + return nullptr; + } + + /** Add a port. Called only during init or restore. */ + virtual void add_port(RunContext& context, EnginePort* port) { + const uint32_t index = port->graph_port()->index(); + if (_ports.size() <= index) { + _ports.resize(index + 1); + } + _ports[index] = port; + } + + /** Remove a port. Called only during init or restore. */ + virtual void remove_port(RunContext& context, EnginePort* port) { + const uint32_t index = port->graph_port()->index(); + _ports[index] = nullptr; + } + + /** Unused since LV2 has no dynamic ports. */ + virtual void register_port(EnginePort& port) {} + + /** Unused since LV2 has no dynamic ports. */ + virtual void unregister_port(EnginePort& port) {} + + /** Unused since LV2 has no dynamic ports. */ + virtual void rename_port(const Raul::Path& old_path, + const Raul::Path& new_path) {} + + /** Unused since LV2 has no dynamic ports. */ + virtual void port_property(const Raul::Path& path, + const URI& uri, + const Atom& value) {} + + virtual EnginePort* create_port(DuplexPort* graph_port) { + graph_port->set_is_driver_port(*_engine.buffer_factory()); + return new EnginePort(graph_port); + } + + virtual void append_time_events(RunContext& context, + Buffer& buffer) + { + const URIs& uris = _engine.world()->uris(); + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)_ports[0]->buffer(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + if (ev->body.type == uris.atom_Object) { + const LV2_Atom_Object* obj = (LV2_Atom_Object*)&ev->body; + if (obj->body.otype == uris.time_Position) { + buffer.append_event(ev->time.frames, + ev->body.size, + ev->body.type, + (const uint8_t*)(&ev->body + 1)); + } + } + } + } + + virtual int real_time_priority() { return 60; } + + /** Called in run thread for events received at control input port. */ + bool enqueue_message(const LV2_Atom* atom) { + if (_from_ui.write(lv2_atom_total_size(atom), atom) == 0) { +#ifndef NDEBUG + _engine.log().error("Control input buffer overflow\n"); +#endif + return false; + } + return true; + } + + Raul::Semaphore& main_sem() { return _main_sem; } + + /** AtomSink::write implementation called by the PostProcessor in the main + * thread to write responses to the UI. + */ + bool write(const LV2_Atom* atom, int32_t default_id) { + // Called from post-processor in main thread + while (_to_ui.write(lv2_atom_total_size(atom), atom) == 0) { + // Overflow, wait until ring is drained next cycle + _to_ui_overflow = true; + _to_ui_overflow_sem.wait(); + _to_ui_overflow = false; + } + return true; + } + + void consume_from_ui() { + const uint32_t read_space = _from_ui.read_space(); + void* buf = nullptr; + for (uint32_t read = 0; read < read_space;) { + LV2_Atom atom; + if (!_from_ui.read(sizeof(LV2_Atom), &atom)) { + _engine.log().rt_error("Error reading head from from-UI ring\n"); + break; + } + + buf = realloc(buf, sizeof(LV2_Atom) + atom.size); + memcpy(buf, &atom, sizeof(LV2_Atom)); + + if (!_from_ui.read(atom.size, (char*)buf + sizeof(LV2_Atom))) { + _engine.log().rt_error("Error reading body from from-UI ring\n"); + break; + } + + _reader.write((LV2_Atom*)buf); + read += sizeof(LV2_Atom) + atom.size; + } + free(buf); + } + + void flush_to_ui(RunContext& context) { + if (_ports.size() < 2) { + _engine.log().rt_error("Standard control ports are not present\n"); + return; + } + + LV2_Atom_Sequence* seq = (LV2_Atom_Sequence*)_ports[1]->buffer(); + if (!seq) { + _engine.log().rt_error("Notify output not connected\n"); + return; + } + + // Initialise output port buffer to an empty Sequence + seq->atom.type = _engine.world()->uris().atom_Sequence; + seq->atom.size = sizeof(LV2_Atom_Sequence_Body); + + const uint32_t read_space = _to_ui.read_space(); + for (uint32_t read = 0; read < read_space;) { + LV2_Atom atom; + if (!_to_ui.peek(sizeof(LV2_Atom), &atom)) { + _engine.log().rt_error("Error reading head from to-UI ring\n"); + break; + } + + if (seq->atom.size + lv2_atom_pad_size( + sizeof(LV2_Atom_Event) + atom.size) + > _notify_capacity) { + break; // Output port buffer full, resume next time + } + + LV2_Atom_Event* ev = (LV2_Atom_Event*)( + (uint8_t*)seq + lv2_atom_total_size(&seq->atom)); + + ev->time.frames = 0; // TODO: Time? + ev->body = atom; + + _to_ui.skip(sizeof(LV2_Atom)); + if (!_to_ui.read(ev->body.size, LV2_ATOM_BODY(&ev->body))) { + _engine.log().rt_error("Error reading body from to-UI ring\n"); + break; + } + + read += lv2_atom_total_size(&ev->body); + seq->atom.size += lv2_atom_pad_size( + sizeof(LV2_Atom_Event) + ev->body.size); + } + + if (_to_ui_overflow) { + _to_ui_overflow_sem.post(); + } + } + + virtual SampleCount block_length() const { return _block_length; } + virtual size_t seq_size() const { return _seq_size; } + virtual SampleCount sample_rate() const { return _sample_rate; } + virtual SampleCount frame_time() const { return _frame_time; } + + AtomReader& reader() { return _reader; } + AtomWriter& writer() { return _writer; } + + typedef std::vector<EnginePort*> Ports; + + Ports& ports() { return _ports; } + + void set_instantiated(bool instantiated) { _instantiated = instantiated; } + +private: + Engine& _engine; + Ports _ports; + Raul::Semaphore _main_sem; + AtomReader _reader; + AtomWriter _writer; + Raul::RingBuffer _from_ui; + Raul::RingBuffer _to_ui; + GraphImpl* _root_graph; + uint32_t _notify_capacity; + SampleCount _block_length; + size_t _seq_size; + SampleCount _sample_rate; + SampleCount _frame_time; + Raul::Semaphore _to_ui_overflow_sem; + bool _to_ui_overflow; + bool _instantiated; +}; + +} // namespace Server +} // namespace Ingen + +extern "C" { + +using namespace Ingen; +using namespace Ingen::Server; + +static void +ingen_lv2_main(SPtr<Engine> engine, const SPtr<LV2Driver>& driver) +{ + while (true) { + // Wait until there is work to be done + driver->main_sem().wait(); + + // Convert pending messages to events and push to pre processor + driver->consume_from_ui(); + + // Run post processor and maid to finalise events from last time + if (!engine->main_iteration()) { + return; + } + } +} + +struct IngenPlugin { + IngenPlugin() + : world(nullptr) + , main(nullptr) + , map(nullptr) + , argc(0) + , argv(nullptr) + {} + + Ingen::World* world; + SPtr<Engine> engine; + std::thread* main; + LV2_URID_Map* map; + int argc; + char** argv; +}; + +static Lib::Graphs +find_graphs(const URI& manifest_uri) +{ + Sord::World world; + Parser parser; + + const std::set<Parser::ResourceRecord> resources = parser.find_resources( + world, + manifest_uri, + URI(INGEN__Graph)); + + Lib::Graphs graphs; + for (const auto& r : resources) { + graphs.push_back(SPtr<const LV2Graph>(new LV2Graph(r))); + } + + return graphs; +} + +static LV2_Handle +ingen_instantiate(const LV2_Descriptor* descriptor, + double rate, + const char* bundle_path, + const LV2_Feature*const* features) +{ + // Get features from features array + LV2_URID_Map* map = nullptr; + LV2_URID_Unmap* unmap = nullptr; + LV2_Log_Log* log = nullptr; + const LV2_Options_Option* options = nullptr; + for (int i = 0; features[i]; ++i) { + if (!strcmp(features[i]->URI, LV2_URID__map)) { + map = (LV2_URID_Map*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_URID__unmap)) { + unmap = (LV2_URID_Unmap*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_LOG__log)) { + log = (LV2_Log_Log*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_OPTIONS__options)) { + options = (const LV2_Options_Option*)features[i]->data; + } + } + + LV2_Log_Logger logger; + lv2_log_logger_init(&logger, map, log); + + if (!map) { + lv2_log_error(&logger, "host did not provide URI map feature\n"); + return nullptr; + } else if (!unmap) { + lv2_log_error(&logger, "host did not provide URI unmap feature\n"); + return nullptr; + } + + set_bundle_path(bundle_path); + const std::string manifest_path = Ingen::bundle_file_path("manifest.ttl"); + SerdNode manifest_node = serd_node_new_file_uri( + (const uint8_t*)manifest_path.c_str(), nullptr, nullptr, true); + + Lib::Graphs graphs = find_graphs(URI((const char*)manifest_node.buf)); + serd_node_free(&manifest_node); + + const LV2Graph* graph = nullptr; + for (const auto& g : graphs) { + if (g->uri == descriptor->URI) { + graph = g.get(); + break; + } + } + + if (!graph) { + lv2_log_error(&logger, "could not find graph <%s>\n", descriptor->URI); + return nullptr; + } + + IngenPlugin* plugin = new IngenPlugin(); + plugin->map = map; + plugin->world = new Ingen::World(map, unmap, log); + plugin->world->load_configuration(plugin->argc, plugin->argv); + + LV2_URID bufsz_max = map->map(map->handle, LV2_BUF_SIZE__maxBlockLength); + LV2_URID bufsz_seq = map->map(map->handle, LV2_BUF_SIZE__sequenceSize); + LV2_URID atom_Int = map->map(map->handle, LV2_ATOM__Int); + int32_t block_length = 0; + int32_t seq_size = 0; + if (options) { + for (const LV2_Options_Option* o = options; o->key; ++o) { + if (o->key == bufsz_max && o->type == atom_Int) { + block_length = *(const int32_t*)o->value; + } else if (o->key == bufsz_seq && o->type == atom_Int) { + seq_size = *(const int32_t*)o->value; + } + } + } + if (block_length == 0) { + block_length = 4096; + plugin->world->log().warn("No maximum block length given\n"); + } + if (seq_size == 0) { + seq_size = 16384; + plugin->world->log().warn("No maximum sequence size given\n"); + } + + plugin->world->log().info( + fmt("Block: %1% frames, Sequence: %2% bytes\n") + % block_length % seq_size); + plugin->world->conf().set( + "queue-size", + plugin->world->forge().make(std::max(block_length, seq_size) * 4)); + + SPtr<Server::Engine> engine(new Server::Engine(plugin->world)); + plugin->engine = engine; + plugin->world->set_engine(engine); + + SPtr<Interface> interface = engine->interface(); + + plugin->world->set_interface(interface); + + Server::ThreadManager::set_flag(Server::THREAD_PRE_PROCESS); + Server::ThreadManager::single_threaded = true; + + LV2Driver* driver = new LV2Driver(*engine.get(), block_length, seq_size, rate); + engine->set_driver(SPtr<Ingen::Server::Driver>(driver)); + + engine->activate(); + Server::ThreadManager::single_threaded = true; + + std::lock_guard<std::mutex> lock(plugin->world->rdf_mutex()); + + // Locate to time 0 to process initialization events + engine->locate(0, block_length); + engine->post_processor()->set_end_time(block_length); + + // Parse graph, filling the queue with events to create it + plugin->world->interface()->bundle_begin(); + plugin->world->parser()->parse_file(plugin->world, + plugin->world->interface().get(), + graph->filename); + plugin->world->interface()->bundle_end(); + + // Drain event queue + while (engine->pending_events()) { + engine->process_all_events(); + engine->post_processor()->process(); + engine->maid()->cleanup(); + } + + /* Register client after loading graph so the to-ui ring does not overflow. + Since we are not yet rolling, it won't be drained, causing a deadlock. */ + SPtr<Interface> client(&driver->writer(), NullDeleter<Interface>); + interface->set_respondee(client); + engine->register_client(client); + + driver->set_instantiated(true); + return (LV2_Handle)plugin; +} + +static void +ingen_connect_port(LV2_Handle instance, uint32_t port, void* data) +{ + using namespace Ingen::Server; + + IngenPlugin* me = (IngenPlugin*)instance; + Server::Engine* engine = (Server::Engine*)me->world->engine().get(); + const SPtr<LV2Driver>& driver = static_ptr_cast<LV2Driver>(engine->driver()); + if (port < driver->ports().size()) { + driver->ports().at(port)->set_buffer(data); + } else { + engine->log().rt_error("Connect to non-existent port\n"); + } +} + +static void +ingen_activate(LV2_Handle instance) +{ + IngenPlugin* me = (IngenPlugin*)instance; + SPtr<Server::Engine> engine = static_ptr_cast<Server::Engine>(me->world->engine()); + const SPtr<LV2Driver>& driver = static_ptr_cast<LV2Driver>(engine->driver()); + engine->activate(); + me->main = new std::thread(ingen_lv2_main, engine, driver); +} + +static void +ingen_run(LV2_Handle instance, uint32_t sample_count) +{ + IngenPlugin* me = (IngenPlugin*)instance; + SPtr<Server::Engine> engine = static_ptr_cast<Server::Engine>(me->world->engine()); + const SPtr<LV2Driver>& driver = static_ptr_cast<LV2Driver>(engine->driver()); + + Server::ThreadManager::set_flag(Ingen::Server::THREAD_PROCESS); + Server::ThreadManager::set_flag(Ingen::Server::THREAD_IS_REAL_TIME); + + driver->run(sample_count); +} + +static void +ingen_deactivate(LV2_Handle instance) +{ + IngenPlugin* me = (IngenPlugin*)instance; + me->world->engine()->deactivate(); + if (me->main) { + me->main->join(); + delete me->main; + me->main = nullptr; + } +} + +static void +ingen_cleanup(LV2_Handle instance) +{ + IngenPlugin* me = (IngenPlugin*)instance; + me->world->set_engine(SPtr<Ingen::Server::Engine>()); + me->world->set_interface(SPtr<Ingen::Interface>()); + if (me->main) { + me->main->join(); + delete me->main; + } + + World* world = me->world; + delete me; + delete world; +} + +static void +get_state_features(const LV2_Feature* const* features, + LV2_State_Map_Path** map, + LV2_State_Make_Path** make) +{ + for (int i = 0; features[i]; ++i) { + if (map && !strcmp(features[i]->URI, LV2_STATE__mapPath)) { + *map = (LV2_State_Map_Path*)features[i]->data; + } else if (make && !strcmp(features[i]->URI, LV2_STATE__makePath)) { + *make = (LV2_State_Make_Path*)features[i]->data; + } + } +} + +static LV2_State_Status +ingen_save(LV2_Handle instance, + LV2_State_Store_Function store, + LV2_State_Handle handle, + uint32_t flags, + const LV2_Feature* const* features) +{ + IngenPlugin* plugin = (IngenPlugin*)instance; + + LV2_State_Map_Path* map_path = nullptr; + LV2_State_Make_Path* make_path = nullptr; + get_state_features(features, &map_path, &make_path); + if (!map_path || !make_path || !plugin->map) { + plugin->world->log().error("Missing state:mapPath, state:makePath, or urid:Map\n"); + return LV2_STATE_ERR_NO_FEATURE; + } + + LV2_URID ingen_file = plugin->map->map(plugin->map->handle, INGEN__file); + LV2_URID atom_Path = plugin->map->map(plugin->map->handle, + LV2_ATOM__Path); + + char* real_path = make_path->path(make_path->handle, "main.ttl"); + char* state_path = map_path->abstract_path(map_path->handle, real_path); + + auto root = plugin->world->store()->find(Raul::Path("/")); + + { + std::lock_guard<std::mutex> lock(plugin->world->rdf_mutex()); + + plugin->world->serialiser()->start_to_file(root->second->path(), real_path); + plugin->world->serialiser()->serialise(root->second); + plugin->world->serialiser()->finish(); + } + + store(handle, + ingen_file, + state_path, + strlen(state_path) + 1, + atom_Path, + LV2_STATE_IS_POD); + + free(state_path); + free(real_path); + return LV2_STATE_SUCCESS; +} + +static LV2_State_Status +ingen_restore(LV2_Handle instance, + LV2_State_Retrieve_Function retrieve, + LV2_State_Handle handle, + uint32_t flags, + const LV2_Feature* const* features) +{ + IngenPlugin* plugin = (IngenPlugin*)instance; + + LV2_State_Map_Path* map_path = nullptr; + get_state_features(features, &map_path, nullptr); + if (!map_path) { + plugin->world->log().error("Missing state:mapPath\n"); + return LV2_STATE_ERR_NO_FEATURE; + } + + LV2_URID ingen_file = plugin->map->map(plugin->map->handle, INGEN__file); + size_t size; + uint32_t type; + uint32_t valflags; + + // Get abstract path to graph file + const char* path = (const char*)retrieve( + handle, ingen_file, &size, &type, &valflags); + if (!path) { + return LV2_STATE_ERR_NO_PROPERTY; + } + + // Convert to absolute path + char* real_path = map_path->absolute_path(map_path->handle, path); + if (!real_path) { + return LV2_STATE_ERR_UNKNOWN; + } + +#if 0 + // Remove existing root graph contents + SPtr<Engine> engine = plugin->engine; + for (const auto& b : engine->root_graph()->blocks()) { + plugin->world->interface()->del(b.uri()); + } + + const uint32_t n_ports = engine->root_graph()->num_ports_non_rt(); + for (int32_t i = n_ports - 1; i >= 0; --i) { + PortImpl* port = engine->root_graph()->port_impl(i); + if (port->symbol() != "control" && port->symbol() != "notify") { + plugin->world->interface()->del(port->uri()); + } + } +#endif + + // Load new graph + std::lock_guard<std::mutex> lock(plugin->world->rdf_mutex()); + plugin->world->parser()->parse_file( + plugin->world, plugin->world->interface().get(), real_path); + + free(real_path); + return LV2_STATE_SUCCESS; +} + +static const void* +ingen_extension_data(const char* uri) +{ + static const LV2_State_Interface state = { ingen_save, ingen_restore }; + if (!strcmp(uri, LV2_STATE__interface)) { + return &state; + } + return nullptr; +} + +LV2Graph::LV2Graph(Parser::ResourceRecord record) + : Parser::ResourceRecord(std::move(record)) +{ + descriptor.URI = uri.c_str(); + descriptor.instantiate = ingen_instantiate; + descriptor.connect_port = ingen_connect_port; + descriptor.activate = ingen_activate; + descriptor.run = ingen_run; + descriptor.deactivate = ingen_deactivate; + descriptor.cleanup = ingen_cleanup; + descriptor.extension_data = ingen_extension_data; +} + +Lib::Lib(const char* bundle_path) +{ + Ingen::set_bundle_path(bundle_path); + const std::string manifest_path = Ingen::bundle_file_path("manifest.ttl"); + SerdNode manifest_node = serd_node_new_file_uri( + (const uint8_t*)manifest_path.c_str(), nullptr, nullptr, true); + + graphs = find_graphs(URI((const char*)manifest_node.buf)); + + serd_node_free(&manifest_node); +} + +static void +lib_cleanup(LV2_Lib_Handle handle) +{ + Lib* lib = (Lib*)handle; + delete lib; +} + +static const LV2_Descriptor* +lib_get_plugin(LV2_Lib_Handle handle, uint32_t index) +{ + Lib* lib = (Lib*)handle; + return index < lib->graphs.size() ? &lib->graphs[index]->descriptor : nullptr; +} + +/** LV2 plugin library entry point */ +LV2_SYMBOL_EXPORT +const LV2_Lib_Descriptor* +lv2_lib_descriptor(const char* bundle_path, + const LV2_Feature*const* features) +{ + static const uint32_t desc_size = sizeof(LV2_Lib_Descriptor); + Lib* lib = new Lib(bundle_path); + + // FIXME: memory leak. I think the LV2_Lib_Descriptor API is botched :( + LV2_Lib_Descriptor* desc = (LV2_Lib_Descriptor*)malloc(desc_size); + desc->handle = lib; + desc->size = desc_size; + desc->cleanup = lib_cleanup; + desc->get_plugin = lib_get_plugin; + + return desc; +} + +} // extern "C" diff --git a/src/server/ingen_portaudio.cpp b/src/server/ingen_portaudio.cpp new file mode 100644 index 00000000..e4065342 --- /dev/null +++ b/src/server/ingen_portaudio.cpp @@ -0,0 +1,54 @@ +/* + This file is part of Ingen. + Copyright 2007-2017 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <string> + +#include "ingen/Atom.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Log.hpp" +#include "ingen/Module.hpp" +#include "ingen/World.hpp" + +#include "PortAudioDriver.hpp" +#include "Engine.hpp" + +using namespace Ingen; + +struct IngenPortAudioModule : public Ingen::Module { + void load(Ingen::World* world) { + if (((Server::Engine*)world->engine().get())->driver()) { + world->log().warn("Engine already has a driver\n"); + return; + } + + Server::PortAudioDriver* driver = new Server::PortAudioDriver( + *(Server::Engine*)world->engine().get()); + driver->attach(); + ((Server::Engine*)world->engine().get())->set_driver( + SPtr<Server::Driver>(driver)); + } +}; + +extern "C" { + +Ingen::Module* +ingen_module_load() +{ + return new IngenPortAudioModule(); +} + +} // extern "C" diff --git a/src/server/internals/BlockDelay.cpp b/src/server/internals/BlockDelay.cpp new file mode 100644 index 00000000..6b27ed83 --- /dev/null +++ b/src/server/internals/BlockDelay.cpp @@ -0,0 +1,89 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <climits> + +#include <cmath> + +#include "ingen/URIs.hpp" +#include "raul/Array.hpp" +#include "raul/Maid.hpp" + +#include "Buffer.hpp" +#include "InputPort.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "RunContext.hpp" +#include "internals/BlockDelay.hpp" + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* BlockDelayNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "BlockDelay"), Raul::Symbol("blockDelay")); +} + +BlockDelayNode::BlockDelayNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, polyphonic, parent, srate) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(2); + + _in_port = new InputPort(bufs, this, Raul::Symbol("in"), 0, 1, + PortType::AUDIO, 0, bufs.forge().make(0.0f)); + _in_port->set_property(uris.lv2_name, bufs.forge().alloc("In")); + _ports->at(0) = _in_port; + + _out_port = new OutputPort(bufs, this, Raul::Symbol("out"), 0, 1, + PortType::AUDIO, 0, bufs.forge().make(0.0f)); + _out_port->set_property(uris.lv2_name, bufs.forge().alloc("Out")); + _ports->at(1) = _out_port; +} + +BlockDelayNode::~BlockDelayNode() +{ + _buffer.reset(); +} + +void +BlockDelayNode::activate(BufferFactory& bufs) +{ + _buffer = bufs.create( + bufs.uris().atom_Sound, 0, bufs.audio_buffer_size()); + + BlockImpl::activate(bufs); +} + +void +BlockDelayNode::run(RunContext& context) +{ + // Copy buffer from last cycle to output + _out_port->buffer(0)->copy(context, _buffer.get()); + + // Copy input from this cycle to buffer + _buffer->copy(context, _in_port->buffer(0).get()); +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/BlockDelay.hpp b/src/server/internals/BlockDelay.hpp new file mode 100644 index 00000000..e1ef5311 --- /dev/null +++ b/src/server/internals/BlockDelay.hpp @@ -0,0 +1,62 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_BLOCKDELAY_HPP +#define INGEN_INTERNALS_BLOCKDELAY_HPP + +#include "BufferRef.hpp" +#include "InternalBlock.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; +class BufferFactory; + +namespace Internals { + +class BlockDelayNode : public InternalBlock +{ +public: + BlockDelayNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + ~BlockDelayNode(); + + void activate(BufferFactory& bufs); + + void run(RunContext& context); + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + InputPort* _in_port; + OutputPort* _out_port; + BufferRef _buffer; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_BLOCKDELAY_HPP diff --git a/src/server/internals/Controller.cpp b/src/server/internals/Controller.cpp new file mode 100644 index 00000000..4c1cf45a --- /dev/null +++ b/src/server/internals/Controller.cpp @@ -0,0 +1,174 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/URIs.hpp" +#include "internals/Controller.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "InputPort.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "PostProcessor.hpp" +#include "RunContext.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* ControllerNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "Controller"), Raul::Symbol("controller")); +} + +ControllerNode::ControllerNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, false, parent, srate) + , _learning(false) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(7); + + const Atom zero = bufs.forge().make(0.0f); + const Atom one = bufs.forge().make(1.0f); + const Atom atom_Float = bufs.forge().make_urid(URI(LV2_ATOM__Float)); + + _midi_in_port = new InputPort(bufs, this, Raul::Symbol("input"), 0, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_in_port->set_property(uris.lv2_name, bufs.forge().alloc("Input")); + _midi_in_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(0) = _midi_in_port; + + _midi_out_port = new OutputPort(bufs, this, Raul::Symbol("event"), 1, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_out_port->set_property(uris.lv2_name, bufs.forge().alloc("Event")); + _midi_out_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(1) = _midi_out_port; + + _param_port = new InputPort(bufs, this, Raul::Symbol("controller"), 2, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _param_port->set_property(uris.atom_supports, atom_Float); + _param_port->set_property(uris.lv2_minimum, zero); + _param_port->set_property(uris.lv2_maximum, bufs.forge().make(127.0f)); + _param_port->set_property(uris.lv2_portProperty, uris.lv2_integer); + _param_port->set_property(uris.lv2_name, bufs.forge().alloc("Controller")); + _ports->at(2) = _param_port; + + _log_port = new InputPort(bufs, this, Raul::Symbol("logarithmic"), 3, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _log_port->set_property(uris.atom_supports, atom_Float); + _log_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _log_port->set_property(uris.lv2_name, bufs.forge().alloc("Logarithmic")); + _ports->at(3) = _log_port; + + _min_port = new InputPort(bufs, this, Raul::Symbol("minimum"), 4, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _min_port->set_property(uris.atom_supports, atom_Float); + _min_port->set_property(uris.lv2_name, bufs.forge().alloc("Minimum")); + _ports->at(4) = _min_port; + + _max_port = new InputPort(bufs, this, Raul::Symbol("maximum"), 5, 1, + PortType::ATOM, uris.atom_Sequence, one); + _max_port->set_property(uris.atom_supports, atom_Float); + _max_port->set_property(uris.lv2_name, bufs.forge().alloc("Maximum")); + _ports->at(5) = _max_port; + + _audio_port = new OutputPort(bufs, this, Raul::Symbol("output"), 6, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _audio_port->set_property(uris.atom_supports, atom_Float); + _audio_port->set_property(uris.lv2_name, bufs.forge().alloc("Output")); + _ports->at(6) = _audio_port; +} + +void +ControllerNode::run(RunContext& context) +{ + const BufferRef midi_in = _midi_in_port->buffer(0); + LV2_Atom_Sequence* seq = midi_in->get<LV2_Atom_Sequence>(); + const BufferRef midi_out = _midi_out_port->buffer(0); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY(&ev->body); + if (ev->body.type == _midi_in_port->bufs().uris().midi_MidiEvent && + ev->body.size >= 3 && + lv2_midi_message_type(buf) == LV2_MIDI_MSG_CONTROLLER) { + if (control(context, buf[1], buf[2], ev->time.frames + context.start())) { + midi_out->append_event(ev->time.frames, &ev->body); + } + } + } +} + +bool +ControllerNode::control(RunContext& context, uint8_t control_num, uint8_t val, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + const uint32_t offset = time - context.start(); + + const Sample nval = (val / 127.0f); // normalized [0, 1] + + if (_learning) { + _param_port->set_control_value(context, time, control_num); + _param_port->force_monitor_update(); + _learning = false; + } else { + _param_port->update_values(offset, 0); + } + + if (control_num != _param_port->buffer(0)->value_at(offset)) { + return false; + } + + for (const auto& port : { _min_port, _max_port, _log_port }) { + port->update_values(offset, 0); + } + + const Sample min_port_val = _min_port->buffer(0)->value_at(offset); + const Sample max_port_val = _max_port->buffer(0)->value_at(offset); + const Sample log_port_val = _log_port->buffer(0)->value_at(offset); + + Sample scaled_value; + if (log_port_val > 0.0f) { + // haaaaack, stupid negatives and logarithms + Sample log_offset = 0; + if (min_port_val < 0) { + log_offset = fabs(min_port_val); + } + const Sample min = log(min_port_val + 1 + log_offset); + const Sample max = log(max_port_val + 1 + log_offset); + scaled_value = expf(nval * (max - min) + min) - 1 - log_offset; + } else { + scaled_value = ((nval) * (max_port_val - min_port_val)) + min_port_val; + } + + _audio_port->set_control_value(context, time, scaled_value); + + return true; +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/Controller.hpp b/src/server/internals/Controller.hpp new file mode 100644 index 00000000..720f78c0 --- /dev/null +++ b/src/server/internals/Controller.hpp @@ -0,0 +1,71 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_CONTROLLER_HPP +#define INGEN_INTERNALS_CONTROLLER_HPP + +#include "InternalBlock.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; + +namespace Internals { + +/** MIDI control input block. + * + * Creating one of these nodes is how a user makes "MIDI Bindings". Note that + * this node will always be monophonic, the poly parameter is ignored. + * + * \ingroup engine + */ +class ControllerNode : public InternalBlock +{ +public: + ControllerNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + void run(RunContext& context); + + bool control(RunContext& context, uint8_t control_num, uint8_t val, FrameTime time); + + void learn() { _learning = true; } + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + InputPort* _midi_in_port; + OutputPort* _midi_out_port; + InputPort* _param_port; + InputPort* _log_port; + InputPort* _min_port; + InputPort* _max_port; + OutputPort* _audio_port; + bool _learning; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_CONTROLLER_HPP diff --git a/src/server/internals/Note.cpp b/src/server/internals/Note.cpp new file mode 100644 index 00000000..b39dd1d4 --- /dev/null +++ b/src/server/internals/Note.cpp @@ -0,0 +1,420 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" +#include "raul/Array.hpp" +#include "raul/Maid.hpp" + +#include "Buffer.hpp" +#include "GraphImpl.hpp" +#include "InputPort.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "RunContext.hpp" +#include "ingen_config.h" +#include "internals/Note.hpp" +#include "util.hpp" + +// #define NOTE_DEBUG 1 + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* NoteNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "Note"), Raul::Symbol("note")); +} + +NoteNode::NoteNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, polyphonic, parent, srate) + , _voices(bufs.maid().make_managed<Voices>(_polyphony)) + , _sustain(false) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(8); + + const Atom zero = bufs.forge().make(0.0f); + const Atom one = bufs.forge().make(1.0f); + + _midi_in_port = new InputPort(bufs, this, Raul::Symbol("input"), 0, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_in_port->set_property(uris.lv2_name, bufs.forge().alloc("Input")); + _midi_in_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(0) = _midi_in_port; + + _freq_port = new OutputPort(bufs, this, Raul::Symbol("frequency"), 1, _polyphony, + PortType::ATOM, uris.atom_Sequence, + bufs.forge().make(440.0f)); + _freq_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _freq_port->set_property(uris.lv2_name, bufs.forge().alloc("Frequency")); + _freq_port->set_property(uris.lv2_minimum, bufs.forge().make(16.0f)); + _freq_port->set_property(uris.lv2_maximum, bufs.forge().make(25088.0f)); + _ports->at(1) = _freq_port; + + _num_port = new OutputPort(bufs, this, Raul::Symbol("number"), 1, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _num_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _num_port->set_property(uris.lv2_minimum, zero); + _num_port->set_property(uris.lv2_maximum, bufs.forge().make(127.0f)); + _num_port->set_property(uris.lv2_portProperty, uris.lv2_integer); + _num_port->set_property(uris.lv2_name, bufs.forge().alloc("Number")); + _ports->at(2) = _num_port; + + _vel_port = new OutputPort(bufs, this, Raul::Symbol("velocity"), 2, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _vel_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _vel_port->set_property(uris.lv2_minimum, zero); + _vel_port->set_property(uris.lv2_maximum, one); + _vel_port->set_property(uris.lv2_name, bufs.forge().alloc("Velocity")); + _ports->at(3) = _vel_port; + + _gate_port = new OutputPort(bufs, this, Raul::Symbol("gate"), 3, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _gate_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _gate_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _gate_port->set_property(uris.lv2_name, bufs.forge().alloc("Gate")); + _ports->at(4) = _gate_port; + + _trig_port = new OutputPort(bufs, this, Raul::Symbol("trigger"), 4, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _trig_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _trig_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _trig_port->set_property(uris.lv2_name, bufs.forge().alloc("Trigger")); + _ports->at(5) = _trig_port; + + _bend_port = new OutputPort(bufs, this, Raul::Symbol("bend"), 5, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _bend_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _bend_port->set_property(uris.lv2_name, bufs.forge().alloc("Bender")); + _bend_port->set_property(uris.lv2_default, zero); + _bend_port->set_property(uris.lv2_minimum, bufs.forge().make(-1.0f)); + _bend_port->set_property(uris.lv2_maximum, one); + _ports->at(6) = _bend_port; + + _pressure_port = new OutputPort(bufs, this, Raul::Symbol("pressure"), 6, _polyphony, + PortType::ATOM, uris.atom_Sequence, zero); + _pressure_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _pressure_port->set_property(uris.lv2_name, bufs.forge().alloc("Pressure")); + _pressure_port->set_property(uris.lv2_default, zero); + _pressure_port->set_property(uris.lv2_minimum, zero); + _pressure_port->set_property(uris.lv2_maximum, one); + _ports->at(7) = _pressure_port; +} + +bool +NoteNode::prepare_poly(BufferFactory& bufs, uint32_t poly) +{ + if (!_polyphonic) { + return true; + } + + BlockImpl::prepare_poly(bufs, poly); + + if (_prepared_voices && poly <= _prepared_voices->size()) { + return true; + } + + _prepared_voices = bufs.maid().make_managed<Voices>( + poly, *_voices, Voice()); + + return true; +} + +bool +NoteNode::apply_poly(RunContext& context, uint32_t poly) +{ + if (!BlockImpl::apply_poly(context, poly)) { + return false; + } + + if (_prepared_voices) { + assert(_polyphony <= _prepared_voices->size()); + _voices = std::move(_prepared_voices); + } + assert(_polyphony <= _voices->size()); + + return true; +} + +void +NoteNode::run(RunContext& context) +{ + Buffer* const midi_in = _midi_in_port->buffer(0).get(); + LV2_Atom_Sequence* seq = midi_in->get<LV2_Atom_Sequence>(); + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY_CONST(&ev->body); + const FrameTime time = context.start() + (FrameTime)ev->time.frames; + if (ev->body.type == _midi_in_port->bufs().uris().midi_MidiEvent && + ev->body.size >= 3) { + switch (lv2_midi_message_type(buf)) { + case LV2_MIDI_MSG_NOTE_ON: + if (buf[2] == 0) { + note_off(context, buf[1], time); + } else { + note_on(context, buf[1], buf[2], time); + } + break; + case LV2_MIDI_MSG_NOTE_OFF: + note_off(context, buf[1], time); + break; + case LV2_MIDI_MSG_CONTROLLER: + switch (buf[1]) { + case LV2_MIDI_CTL_ALL_NOTES_OFF: + case LV2_MIDI_CTL_ALL_SOUNDS_OFF: + all_notes_off(context, time); + break; + case LV2_MIDI_CTL_SUSTAIN: + if (buf[2] > 63) { + sustain_on(context, time); + } else { + sustain_off(context, time); + } + break; + } + break; + case LV2_MIDI_MSG_BENDER: + bend(context, time, (((((uint16_t)buf[2] << 7) | buf[1]) - 8192.0f) + / 8192.0f)); + break; + case LV2_MIDI_MSG_CHANNEL_PRESSURE: + channel_pressure(context, time, buf[1] / 127.0f); + break; + case LV2_MIDI_MSG_NOTE_PRESSURE: + note_pressure(context, time, buf[1], buf[2] / 127.0f); + break; + default: + break; + } + } + } +} + +static inline float +note_to_freq(uint8_t num) +{ + static const float A4 = 440.0f; + return A4 * powf(2.0f, (float)(num - 57.0f) / 12.0f); +} + +void +NoteNode::note_on(RunContext& context, uint8_t note_num, uint8_t velocity, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + assert(note_num <= 127); + + Key* key = &_keys[note_num]; + Voice* voice = nullptr; + uint32_t voice_num = 0; + + if (key->state != Key::State::OFF) { + return; + } + + // Look for free voices + for (uint32_t i=0; i < _polyphony; ++i) { + if ((*_voices)[i].state == Voice::State::FREE) { + voice = &(*_voices)[i]; + voice_num = i; + break; + } + } + + // If we didn't find a free one, steal the oldest + if (voice == nullptr) { + voice_num = 0; + voice = &(*_voices)[0]; + FrameTime oldest_time = (*_voices)[0].time; + for (uint32_t i=1; i < _polyphony; ++i) { + if ((*_voices)[i].time < oldest_time) { + voice = &(*_voices)[i]; + voice_num = i; + oldest_time = voice->time; + } + } + } + assert(voice != nullptr); + assert(voice == &(*_voices)[voice_num]); + + // Update stolen key, if applicable + if (voice->state == Voice::State::ACTIVE) { + assert(_keys[voice->note].state == Key::State::ON_ASSIGNED); + assert(_keys[voice->note].voice == voice_num); + _keys[voice->note].state = Key::State::ON_UNASSIGNED; + } + + // Store key information for later reallocation on note off + key->state = Key::State::ON_ASSIGNED; + key->voice = voice_num; + key->time = time; + + // Check if we just triggered this voice at the same time + // (Double note-on at the same sample on the same voice) + const bool double_trigger = (voice->state == Voice::State::ACTIVE && + voice->time == time); + + // Trigger voice + voice->state = Voice::State::ACTIVE; + voice->note = note_num; + voice->time = time; + + assert(_keys[voice->note].state == Key::State::ON_ASSIGNED); + assert(_keys[voice->note].voice == voice_num); + + _freq_port->set_voice_value(context, voice_num, time, note_to_freq(note_num)); + _num_port->set_voice_value(context, voice_num, time, (float)note_num); + _vel_port->set_voice_value(context, voice_num, time, velocity / 127.0f); + _gate_port->set_voice_value(context, voice_num, time, 1.0f); + if (!double_trigger) { + _trig_port->set_voice_value(context, voice_num, time, 1.0f); + _trig_port->set_voice_value(context, voice_num, time + 1, 0.0f); + } + + assert(key->state == Key::State::ON_ASSIGNED); + assert(voice->state == Voice::State::ACTIVE); + assert(key->voice == voice_num); + assert((*_voices)[key->voice].note == note_num); +} + +void +NoteNode::note_off(RunContext& context, uint8_t note_num, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + + Key* key = &_keys[note_num]; + + if (key->state == Key::State::ON_ASSIGNED) { + // Assigned key, turn off voice and key + if ((*_voices)[key->voice].state == Voice::State::ACTIVE) { + assert((*_voices)[key->voice].note == note_num); + if ( ! _sustain) { + free_voice(context, key->voice, time); + } else { + (*_voices)[key->voice].state = Voice::State::HOLDING; + } + } + } + + key->state = Key::State::OFF; +} + +void +NoteNode::free_voice(RunContext& context, uint32_t voice, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + + // Find a key to reassign to the freed voice (the newest, if there is one) + Key* replace_key = nullptr; + uint8_t replace_key_num = 0; + + for (uint8_t i = 0; i <= 127; ++i) { + if (_keys[i].state == Key::State::ON_UNASSIGNED) { + if (replace_key == nullptr || _keys[i].time > replace_key->time) { + replace_key = &_keys[i]; + replace_key_num = i; + } + } + } + + if (replace_key != nullptr) { // Found a key to assign to freed voice + assert(&_keys[replace_key_num] == replace_key); + assert(replace_key->state == Key::State::ON_UNASSIGNED); + + // Change the freq but leave the gate high and don't retrigger + _freq_port->set_voice_value(context, voice, time, note_to_freq(replace_key_num)); + _num_port->set_voice_value(context, voice, time, replace_key_num); + + replace_key->state = Key::State::ON_ASSIGNED; + replace_key->voice = voice; + _keys[(*_voices)[voice].note].state = Key::State::ON_UNASSIGNED; + (*_voices)[voice].note = replace_key_num; + (*_voices)[voice].state = Voice::State::ACTIVE; + } else { + // No new note for voice, deactivate (set gate low) + _gate_port->set_voice_value(context, voice, time, 0.0f); + (*_voices)[voice].state = Voice::State::FREE; + } +} + +void +NoteNode::all_notes_off(RunContext& context, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + + // FIXME: set all keys to Key::OFF? + + for (uint32_t i = 0; i < _polyphony; ++i) { + _gate_port->set_voice_value(context, i, time, 0.0f); + (*_voices)[i].state = Voice::State::FREE; + } +} + +void +NoteNode::sustain_on(RunContext& context, FrameTime time) +{ + _sustain = true; +} + +void +NoteNode::sustain_off(RunContext& context, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + + _sustain = false; + + for (uint32_t i=0; i < _polyphony; ++i) { + if ((*_voices)[i].state == Voice::State::HOLDING) { + free_voice(context, i, time); + } + } +} + +void +NoteNode::bend(RunContext& context, FrameTime time, float amount) +{ + _bend_port->set_control_value(context, time, amount); +} + +void +NoteNode::note_pressure(RunContext& context, FrameTime time, uint8_t note_num, float amount) +{ + for (uint32_t i=0; i < _polyphony; ++i) { + if ((*_voices)[i].state != Voice::State::FREE && (*_voices)[i].note == note_num) { + _pressure_port->set_voice_value(context, i, time, amount); + return; + } + } +} + +void +NoteNode::channel_pressure(RunContext& context, FrameTime time, float amount) +{ + _pressure_port->set_control_value(context, time, amount); +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/Note.hpp b/src/server/internals/Note.hpp new file mode 100644 index 00000000..1e60c130 --- /dev/null +++ b/src/server/internals/Note.hpp @@ -0,0 +1,109 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_NOTE_HPP +#define INGEN_INTERNALS_NOTE_HPP + +#include "InternalBlock.hpp" +#include "types.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; + +namespace Internals { + +/** MIDI note input block. + * + * For pitched instruments like keyboard, etc. + * + * \ingroup engine + */ +class NoteNode : public InternalBlock +{ +public: + NoteNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + bool prepare_poly(BufferFactory& bufs, uint32_t poly); + bool apply_poly(RunContext& context, uint32_t poly); + + void run(RunContext& context); + + void note_on(RunContext& context, uint8_t note_num, uint8_t velocity, FrameTime time); + void note_off(RunContext& context, uint8_t note_num, FrameTime time); + void all_notes_off(RunContext& context, FrameTime time); + + void sustain_on(RunContext& context, FrameTime time); + void sustain_off(RunContext& context, FrameTime time); + + void bend(RunContext& context, FrameTime time, float amount); + void note_pressure(RunContext& context, FrameTime time, uint8_t note_num, float amount); + void channel_pressure(RunContext& context, FrameTime time, float amount); + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + /** Key, one for each key on the keyboard */ + struct Key { + enum class State { OFF, ON_ASSIGNED, ON_UNASSIGNED }; + Key() : state(State::OFF), voice(0), time(0) {} + State state; + uint32_t voice; + SampleCount time; + }; + + /** Voice, one of these always exists for each voice */ + struct Voice { + enum class State { FREE, ACTIVE, HOLDING }; + Voice() : state(State::FREE), note(0), time(0) {} + State state; + uint8_t note; + SampleCount time; + }; + + typedef Raul::Array<Voice> Voices; + + void free_voice(RunContext& context, uint32_t voice, FrameTime time); + + MPtr<Voices> _voices; + MPtr<Voices> _prepared_voices; + + Key _keys[128]; + bool _sustain; ///< Whether or not hold pedal is depressed + + InputPort* _midi_in_port; + OutputPort* _freq_port; + OutputPort* _num_port; + OutputPort* _vel_port; + OutputPort* _gate_port; + OutputPort* _trig_port; + OutputPort* _bend_port; + OutputPort* _pressure_port; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_NOTE_HPP diff --git a/src/server/internals/Time.cpp b/src/server/internals/Time.cpp new file mode 100644 index 00000000..5474bf21 --- /dev/null +++ b/src/server/internals/Time.cpp @@ -0,0 +1,78 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "Buffer.hpp" +#include "Driver.hpp" +#include "Engine.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "RunContext.hpp" +#include "internals/Time.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* TimeNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "Time"), Raul::Symbol("time")); +} + +TimeNode::TimeNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, false, parent, srate) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(1); + + _notify_port = new OutputPort( + bufs, this, Raul::Symbol("notify"), 0, 1, + PortType::ATOM, uris.atom_Sequence, Atom(), 1024); + _notify_port->set_property(uris.lv2_name, bufs.forge().alloc("Notify")); + _notify_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.time_Position)); + _ports->at(0) = _notify_port; +} + +void +TimeNode::run(RunContext& context) +{ + BufferRef buf = _notify_port->buffer(0); + LV2_Atom_Sequence* seq = buf->get<LV2_Atom_Sequence>(); + + // Initialise output to the empty sequence + seq->atom.type = _notify_port->bufs().uris().atom_Sequence; + seq->atom.size = sizeof(LV2_Atom_Sequence_Body); + seq->body.unit = 0; + seq->body.pad = 0; + + // Ask the driver to append any time events for this cycle + context.engine().driver()->append_time_events( + context, *_notify_port->buffer(0)); +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/Time.hpp b/src/server/internals/Time.hpp new file mode 100644 index 00000000..1a063f8d --- /dev/null +++ b/src/server/internals/Time.hpp @@ -0,0 +1,59 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_TIME_HPP +#define INGEN_INTERNALS_TIME_HPP + +#include "InternalBlock.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; + +namespace Internals { + +/** Time information block. + * + * This sends messages whenever the transport speed or tempo changes. + * + * \ingroup engine + */ +class TimeNode : public InternalBlock +{ +public: + TimeNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + void run(RunContext& context); + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + OutputPort* _notify_port; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_TIME_HPP diff --git a/src/server/internals/Trigger.cpp b/src/server/internals/Trigger.cpp new file mode 100644 index 00000000..69967877 --- /dev/null +++ b/src/server/internals/Trigger.cpp @@ -0,0 +1,187 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <cmath> + +#include "ingen/URIs.hpp" +#include "lv2/lv2plug.in/ns/ext/atom/util.h" +#include "lv2/lv2plug.in/ns/ext/midi/midi.h" + +#include "Buffer.hpp" +#include "Engine.hpp" +#include "InputPort.hpp" +#include "InternalPlugin.hpp" +#include "OutputPort.hpp" +#include "RunContext.hpp" +#include "ingen_config.h" +#include "internals/Trigger.hpp" +#include "util.hpp" + +namespace Ingen { +namespace Server { +namespace Internals { + +InternalPlugin* TriggerNode::internal_plugin(URIs& uris) { + return new InternalPlugin( + uris, URI(NS_INTERNALS "Trigger"), Raul::Symbol("trigger")); +} + +TriggerNode::TriggerNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate) + : InternalBlock(plugin, symbol, false, parent, srate) + , _learning(false) +{ + const Ingen::URIs& uris = bufs.uris(); + _ports = bufs.maid().make_managed<Ports>(6); + + const Atom zero = bufs.forge().make(0.0f); + + _midi_in_port = new InputPort(bufs, this, Raul::Symbol("input"), 0, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_in_port->set_property(uris.lv2_name, bufs.forge().alloc("Input")); + _midi_in_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(0) = _midi_in_port; + + _midi_out_port = new OutputPort(bufs, this, Raul::Symbol("event"), 1, 1, + PortType::ATOM, uris.atom_Sequence, Atom()); + _midi_out_port->set_property(uris.lv2_name, bufs.forge().alloc("Event")); + _midi_out_port->set_property(uris.atom_supports, + bufs.forge().make_urid(uris.midi_MidiEvent)); + _ports->at(1) = _midi_out_port; + + _note_port = new InputPort(bufs, this, Raul::Symbol("note"), 2, 1, + PortType::ATOM, uris.atom_Sequence, + bufs.forge().make(60.0f)); + _note_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _note_port->set_property(uris.lv2_minimum, zero); + _note_port->set_property(uris.lv2_maximum, bufs.forge().make(127.0f)); + _note_port->set_property(uris.lv2_portProperty, uris.lv2_integer); + _note_port->set_property(uris.lv2_name, bufs.forge().alloc("Note")); + _ports->at(2) = _note_port; + + _gate_port = new OutputPort(bufs, this, Raul::Symbol("gate"), 3, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _gate_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _gate_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _gate_port->set_property(uris.lv2_name, bufs.forge().alloc("Gate")); + _ports->at(3) = _gate_port; + + _trig_port = new OutputPort(bufs, this, Raul::Symbol("trigger"), 4, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _trig_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _trig_port->set_property(uris.lv2_portProperty, uris.lv2_toggled); + _trig_port->set_property(uris.lv2_name, bufs.forge().alloc("Trigger")); + _ports->at(4) = _trig_port; + + _vel_port = new OutputPort(bufs, this, Raul::Symbol("velocity"), 5, 1, + PortType::ATOM, uris.atom_Sequence, zero); + _vel_port->set_property(uris.atom_supports, bufs.uris().atom_Float); + _vel_port->set_property(uris.lv2_minimum, zero); + _vel_port->set_property(uris.lv2_maximum, bufs.forge().make(1.0f)); + _vel_port->set_property(uris.lv2_name, bufs.forge().alloc("Velocity")); + _ports->at(5) = _vel_port; +} + +void +TriggerNode::run(RunContext& context) +{ + const BufferRef midi_in = _midi_in_port->buffer(0); + LV2_Atom_Sequence* const seq = midi_in->get<LV2_Atom_Sequence>(); + const BufferRef midi_out = _midi_out_port->buffer(0); + + // Initialise output to the empty sequence + midi_out->prepare_write(context); + + LV2_ATOM_SEQUENCE_FOREACH(seq, ev) { + const int64_t t = ev->time.frames; + const uint8_t* buf = (const uint8_t*)LV2_ATOM_BODY(&ev->body); + bool emit = false; + if (ev->body.type == _midi_in_port->bufs().uris().midi_MidiEvent && + ev->body.size >= 3) { + const FrameTime time = context.start() + t; + switch (lv2_midi_message_type(buf)) { + case LV2_MIDI_MSG_NOTE_ON: + if (buf[2] == 0) { + emit = note_off(context, buf[1], time); + } else { + emit = note_on(context, buf[1], buf[2], time); + } + break; + case LV2_MIDI_MSG_NOTE_OFF: + emit = note_off(context, buf[1], time); + break; + case LV2_MIDI_MSG_CONTROLLER: + switch (buf[1]) { + case LV2_MIDI_CTL_ALL_NOTES_OFF: + case LV2_MIDI_CTL_ALL_SOUNDS_OFF: + _gate_port->set_control_value(context, time, 0.0f); + emit = true; + } + default: + break; + } + } + + if (emit) { + midi_out->append_event(t, &ev->body); + } + } +} + +bool +TriggerNode::note_on(RunContext& context, uint8_t note_num, uint8_t velocity, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + const uint32_t offset = time - context.start(); + + if (_learning) { + _note_port->set_control_value(context, time, (float)note_num); + _note_port->force_monitor_update(); + _learning = false; + } + + if (note_num == lrintf(_note_port->buffer(0)->value_at(offset))) { + _gate_port->set_control_value(context, time, 1.0f); + _trig_port->set_control_value(context, time, 1.0f); + _trig_port->set_control_value(context, time + 1, 0.0f); + _vel_port->set_control_value(context, time, velocity / 127.0f); + return true; + } + return false; +} + +bool +TriggerNode::note_off(RunContext& context, uint8_t note_num, FrameTime time) +{ + assert(time >= context.start() && time <= context.end()); + const uint32_t offset = time - context.start(); + + if (note_num == lrintf(_note_port->buffer(0)->value_at(offset))) { + _gate_port->set_control_value(context, time, 0.0f); + return true; + } + + return false; +} + +} // namespace Internals +} // namespace Server +} // namespace Ingen diff --git a/src/server/internals/Trigger.hpp b/src/server/internals/Trigger.hpp new file mode 100644 index 00000000..4d67395a --- /dev/null +++ b/src/server/internals/Trigger.hpp @@ -0,0 +1,75 @@ +/* + This file is part of Ingen. + Copyright 2007-2016 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_INTERNALS_TRIGGER_HPP +#define INGEN_INTERNALS_TRIGGER_HPP + +#include "InternalBlock.hpp" + +namespace Ingen { +namespace Server { + +class InputPort; +class OutputPort; +class InternalPlugin; + +namespace Internals { + +/** MIDI trigger input block. + * + * Just has a gate, for drums etc. A control port is used to select + * which note number is responded to. + * + * Note that this block is always monophonic, the poly parameter is ignored. + * (Should that change?) + * + * \ingroup engine + */ +class TriggerNode : public InternalBlock +{ +public: + TriggerNode(InternalPlugin* plugin, + BufferFactory& bufs, + const Raul::Symbol& symbol, + bool polyphonic, + GraphImpl* parent, + SampleRate srate); + + void run(RunContext& context); + + bool note_on(RunContext& context, uint8_t note_num, uint8_t velocity, FrameTime time); + bool note_off(RunContext& context, uint8_t note_num, FrameTime time); + + void learn() { _learning = true; } + + static InternalPlugin* internal_plugin(URIs& uris); + +private: + bool _learning; + + InputPort* _midi_in_port; + OutputPort* _midi_out_port; + InputPort* _note_port; + OutputPort* _gate_port; + OutputPort* _trig_port; + OutputPort* _vel_port; +}; + +} // namespace Server +} // namespace Ingen +} // namespace Internals + +#endif // INGEN_INTERNALS_TRIGGER_HPP diff --git a/src/server/jackey.h b/src/server/jackey.h new file mode 100644 index 00000000..fc31d73c --- /dev/null +++ b/src/server/jackey.h @@ -0,0 +1,72 @@ +/* + Copyright 2014-2015 David Robillard <http://drobilla.net> + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +/** + The supported event types of an event port. + + This is a kludge around Jack only supporting MIDI, particularly for OSC. + This property is a comma-separated list of event types, currently "MIDI" or + "OSC". If this contains "OSC", the port may carry OSC bundles (first byte + '#') or OSC messages (first byte '/'). Note that the "status byte" of both + OSC events is not a valid MIDI status byte, so MIDI clients that check the + status byte will gracefully ignore OSC messages if the user makes an + inappropriate connection. +*/ +#define JACKEY_EVENT_TYPES "http://jackaudio.org/metadata/event-types" + +/** + The type of an audio signal. + + This property allows audio ports to be tagged with a "meaning". The value + is a simple string. Currently, the only type is "CV", for "control voltage" + ports. Hosts SHOULD be take care to not treat CV ports as audibile and send + their output directly to speakers. In particular, CV ports are not + necessarily periodic at all and may have very high DC. +*/ +#define JACKEY_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type" + +/** + The name of the icon for the subject (typically client). + + This is used for looking up icons on the system, possibly with many sizes or + themes. Icons should be searched for according to the freedesktop Icon + Theme Specification: + + http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html +*/ +#define JACKEY_ICON_NAME "http://jackaudio.org/metadata/icon-name" + +/** + Channel designation for a port. + + This allows ports to be tagged with a meaningful designation like "left", + "right", "lfe", etc. + + The value MUST be a URI. An extensive set of URIs for designating audio + channels can be found at http://lv2plug.in/ns/ext/port-groups +*/ +#define JACKEY_DESIGNATION "http://lv2plug.in/ns/lv2core#designation" + +/** + Order for a port. + + This is used to specify the best order to show ports in user interfaces. + The value MUST be an integer. There are no other requirements, so there may + be gaps in the orders for several ports. Applications should compare the + orders of ports to determine their relative order, but must not assign any + other relevance to order values. +*/ +#define JACKEY_ORDER "http://jackaudio.org/metadata/order" diff --git a/src/server/mix.cpp b/src/server/mix.cpp new file mode 100644 index 00000000..3e7634fe --- /dev/null +++ b/src/server/mix.cpp @@ -0,0 +1,112 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "lv2/lv2plug.in/ns/ext/atom/util.h" + +#include "Buffer.hpp" +#include "RunContext.hpp" +#include "mix.hpp" + +namespace Ingen { +namespace Server { + +static inline bool +is_end(const Buffer* buf, const LV2_Atom_Event* ev) +{ + const LV2_Atom* atom = buf->get<const LV2_Atom>(); + return lv2_atom_sequence_is_end( + (const LV2_Atom_Sequence_Body*)LV2_ATOM_BODY_CONST(atom), + atom->size, + ev); +} + +void +mix(const RunContext& context, + Buffer* dst, + const Buffer*const* srcs, + uint32_t num_srcs) +{ + if (num_srcs == 1) { + dst->copy(context, srcs[0]); + } else if (dst->is_control()) { + Sample* const out = dst->samples(); + out[0] = srcs[0]->value_at(0); + for (uint32_t i = 1; i < num_srcs; ++i) { + out[0] += srcs[i]->value_at(0); + } + } else if (dst->is_audio()) { + // Copy the first source + dst->copy(context, srcs[0]); + + // Mix in the rest + Sample* __restrict const out = dst->samples(); + const SampleCount end = context.nframes(); + for (uint32_t i = 1; i < num_srcs; ++i) { + const Sample* __restrict const in = srcs[i]->samples(); + if (srcs[i]->is_control()) { // control => audio + for (SampleCount i = 0; i < end; ++i) { + out[i] += in[0]; + } + } else if (srcs[i]->is_audio()) { // audio => audio + for (SampleCount i = 0; i < end; ++i) { + out[i] += in[i]; + } + } else if (srcs[i]->is_sequence()) { // sequence => audio + dst->render_sequence(context, srcs[i], true); + } + } + } else if (dst->is_sequence()) { + const LV2_Atom_Event* iters[num_srcs]; + for (uint32_t i = 0; i < num_srcs; ++i) { + iters[i] = nullptr; + if (srcs[i]->is_sequence()) { + const LV2_Atom_Sequence* seq = srcs[i]->get<const LV2_Atom_Sequence>(); + iters[i] = lv2_atom_sequence_begin(&seq->body); + if (is_end(srcs[i], iters[i])) { + iters[i] = nullptr; + } + } + } + + while (true) { + const LV2_Atom_Event* first = nullptr; + uint32_t first_i = 0; + for (uint32_t i = 0; i < num_srcs; ++i) { + const LV2_Atom_Event* const ev = iters[i]; + if (!first || (ev && ev->time.frames < first->time.frames)) { + first = ev; + first_i = i; + } + } + + if (first) { + dst->append_event( + first->time.frames, first->body.size, first->body.type, + (const uint8_t*)LV2_ATOM_BODY_CONST(&first->body)); + + iters[first_i] = lv2_atom_sequence_next(first); + if (is_end(srcs[first_i], iters[first_i])) { + iters[first_i] = nullptr; + } + } else { + break; + } + } + } +} + +} // namespace Server +} // namespace Ingen diff --git a/src/server/mix.hpp b/src/server/mix.hpp new file mode 100644 index 00000000..3d8880db --- /dev/null +++ b/src/server/mix.hpp @@ -0,0 +1,40 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_MIX_HPP +#define INGEN_ENGINE_MIX_HPP + +#include <cstdint> + +namespace Ingen { + +class URIs; + +namespace Server { + +class Buffer; +class RunContext; + +void +mix(const RunContext& context, + Buffer* dst, + const Buffer*const* srcs, + uint32_t num_srcs); + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_MIX_HPP diff --git a/src/server/types.hpp b/src/server/types.hpp new file mode 100644 index 00000000..e7dae117 --- /dev/null +++ b/src/server/types.hpp @@ -0,0 +1,27 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_TYPES_HPP +#define INGEN_ENGINE_TYPES_HPP + +#include <cstdint> + +typedef float Sample; +typedef uint32_t SampleCount; +typedef uint32_t SampleRate; +typedef uint32_t FrameTime; + +#endif // INGEN_ENGINE_TYPES_HPP diff --git a/src/server/util.hpp b/src/server/util.hpp new file mode 100644 index 00000000..7d30cc8f --- /dev/null +++ b/src/server/util.hpp @@ -0,0 +1,63 @@ +/* + This file is part of Ingen. + Copyright 2007-2015 David Robillard <http://drobilla.net/> + + Ingen is free software: you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or any later version. + + Ingen is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for details. + + You should have received a copy of the GNU Affero General Public License + along with Ingen. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef INGEN_ENGINE_UTIL_HPP +#define INGEN_ENGINE_UTIL_HPP + +#include <cstdlib> + +#include "ingen/Log.hpp" +#include "raul/Path.hpp" + +#include "ingen_config.h" + +#include <fenv.h> +#ifdef __SSE__ +#include <xmmintrin.h> +#endif + +#ifdef __clang__ +# define REALTIME __attribute__((annotate("realtime"))) +#else +# define REALTIME +#endif + +#if defined(INGEN_HAVE_THREAD_LOCAL) +# define INGEN_THREAD_LOCAL thread_local +#elif defined(INGEN_HAVE_THREAD_BUILTIN) +# define INGEN_THREAD_LOCAL __thread +#else +# define INGEN_THREAD_LOCAL +#endif + +namespace Ingen { +namespace Server { + +/** Set flags to disable denormal processing. + */ +inline void +set_denormal_flags(Ingen::Log& log) +{ +#ifdef __SSE__ + _mm_setcsr(_mm_getcsr() | 0x8040); + log.info("Set SSE denormal-are-zero and flush-to-zero flags\n"); +#endif +} + +} // namespace Server +} // namespace Ingen + +#endif // INGEN_ENGINE_UTIL_HPP diff --git a/src/server/wscript b/src/server/wscript new file mode 100644 index 00000000..8d1ec90d --- /dev/null +++ b/src/server/wscript @@ -0,0 +1,104 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf + +def build(bld): + core_source = ''' + ArcImpl.cpp + BlockFactory.cpp + BlockImpl.cpp + Broadcaster.cpp + Buffer.cpp + BufferFactory.cpp + CompiledGraph.cpp + ClientUpdate.cpp + ControlBindings.cpp + DuplexPort.cpp + Engine.cpp + EventWriter.cpp + GraphImpl.cpp + InputPort.cpp + InternalBlock.cpp + InternalPlugin.cpp + LV2Block.cpp + LV2Plugin.cpp + NodeImpl.cpp + PortImpl.cpp + PostProcessor.cpp + PreProcessor.cpp + RunContext.cpp + SocketListener.cpp + Task.cpp + UndoStack.cpp + Worker.cpp + events/Connect.cpp + events/Copy.cpp + events/CreateBlock.cpp + events/CreateGraph.cpp + events/CreatePort.cpp + events/Delete.cpp + events/Delta.cpp + events/Disconnect.cpp + events/DisconnectAll.cpp + events/Get.cpp + events/Mark.cpp + events/Move.cpp + events/SetPortValue.cpp + events/Undo.cpp + ingen_engine.cpp + internals/BlockDelay.cpp + internals/Controller.cpp + internals/Note.cpp + internals/Time.cpp + internals/Trigger.cpp + mix.cpp + ''' + + obj = bld(features = 'cxx cxxshlib', + source = core_source, + export_includes = ['../..'], + includes = ['.', '../..'], + name = 'libingen_server', + target = 'ingen_server', + install_path = '${LIBDIR}', + use = 'libingen libingen_socket', + cxxflags = bld.env.PTHREAD_CFLAGS + bld.env.INGEN_TEST_CXXFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS + bld.env.INGEN_TEST_LINKFLAGS) + core_libs = 'LV2 LILV RAUL SERD SORD' + autowaf.use_lib(bld, obj, core_libs) + + if bld.env.HAVE_JACK: + obj = bld(features = 'cxx cxxshlib', + source = 'JackDriver.cpp ingen_jack.cpp', + includes = ['.', '../..'], + name = 'libingen_jack', + target = 'ingen_jack', + install_path = '${LIBDIR}', + use = 'libingen_server', + cxxflags = bld.env.PTHREAD_CFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS) + autowaf.use_lib(bld, obj, core_libs + ' JACK') + + if bld.env.HAVE_PORTAUDIO: + obj = bld(features = 'cxx cxxshlib', + source = 'PortAudioDriver.cpp ingen_portaudio.cpp', + includes = ['.', '../..'], + name = 'libingen_portaudio', + target = 'ingen_portaudio', + install_path = '${LIBDIR}', + use = 'libingen_server', + cxxflags = bld.env.PTHREAD_CFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS) + autowaf.use_lib(bld, obj, core_libs + ' PORTAUDIO') + + # Ingen LV2 wrapper + if bld.env.INGEN_BUILD_LV2: + obj = bld(features = 'cxx cxxshlib', + source = ' ingen_lv2.cpp ', + includes = ['.', '../..'], + name = 'libingen_lv2', + target = 'ingen_lv2', + install_path = '${LV2DIR}/ingen.lv2/', + use = 'libingen libingen_server', + cxxflags = bld.env.PTHREAD_CFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS) + autowaf.use_lib(bld, obj, core_libs) diff --git a/src/wscript b/src/wscript new file mode 100644 index 00000000..07379b83 --- /dev/null +++ b/src/wscript @@ -0,0 +1,46 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf + +def build(bld): + sources = [ + 'AtomReader.cpp', + 'AtomWriter.cpp', + 'ClashAvoider.cpp', + 'ColorContext.cpp', + 'Configuration.cpp', + 'FilePath.cpp', + 'Forge.cpp', + 'LV2Features.cpp', + 'Library.cpp', + 'Log.cpp', + 'Parser.cpp', + 'Resource.cpp', + 'Serialiser.cpp', + 'Store.cpp', + 'StreamWriter.cpp', + 'TurtleWriter.cpp', + 'URI.cpp', + 'URIMap.cpp', + 'URIs.cpp', + 'World.cpp', + 'runtime_paths.cpp' + ] + if bld.is_defined('HAVE_SOCKET'): + sources += [ 'SocketReader.cpp', 'SocketWriter.cpp' ] + + lib = [] + if bld.is_defined('HAVE_LIBDL'): + lib += ['dl'] + + obj = bld(features = 'cxx cxxshlib', + source = sources, + export_includes = ['..'], + includes = ['..'], + name = 'libingen', + target = 'ingen', + vnum = '0.0.0', + install_path = '${LIBDIR}', + lib = lib, + cxxflags = bld.env.PTHREAD_CFLAGS + bld.env.INGEN_TEST_CXXFLAGS, + linkflags = bld.env.PTHREAD_LINKFLAGS + bld.env.INGEN_TEST_LINKFLAGS) + autowaf.use_lib(bld, obj, 'LV2 LILV RAUL SERD SORD SRATOM') diff --git a/tests/TestClient.hpp b/tests/TestClient.hpp new file mode 100644 index 00000000..8d99db86 --- /dev/null +++ b/tests/TestClient.hpp @@ -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/>. +*/ + +#ifndef INGEN_TESTCLIENT_HPP +#define INGEN_TESTCLIENT_HPP + +#include <boost/variant/get.hpp> + +#include "ingen/Interface.hpp" +#include "ingen/Log.hpp" + +using namespace Ingen; + +class TestClient : public Ingen::Interface +{ +public: + explicit TestClient(Log& log) : _log(log) {} + ~TestClient() {} + + URI uri() const override { return URI("ingen:testClient"); } + + void message(const Message& msg) override { + if (const Response* const response = boost::get<Response>(&msg)) { + if (response->status != Status::SUCCESS) { + _log.error(fmt("error on message %1%: %2% (%3%)\n") + % response->id + % ingen_status_string(response->status) + % response->subject); + exit(EXIT_FAILURE); + } + } else if (const Error* const error = boost::get<Error>(&msg)) { + _log.error(fmt("error: %1%\n") % error->message); + exit(EXIT_FAILURE); + } + } + +private: + Log& _log; +}; + +#endif // INGEN_TESTCLIENT_HPP diff --git a/tests/connect_disconnect_node_node.ttl b/tests/connect_disconnect_node_node.ttl new file mode 100644 index 00000000..935de482 --- /dev/null +++ b/tests/connect_disconnect_node_node.ttl @@ -0,0 +1,36 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/node1> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg1> + a patch:Put ; + patch:subject <ingen:/main/node2> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg2> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/node1/left_out> ; + ingen:head <ingen:/main/node2/left_in> + ] . + +<msg3> + a patch:Delete ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/node1/left_out> ; + ingen:head <ingen:/main/node2/left_in> + ] . diff --git a/tests/connect_disconnect_node_patch.ttl b/tests/connect_disconnect_node_patch.ttl new file mode 100644 index 00000000..77ada2ad --- /dev/null +++ b/tests/connect_disconnect_node_patch.ttl @@ -0,0 +1,105 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/amp> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://lv2plug.in/plugins/eg-amp> + ] . + +<msg1> + a patch:Put ; + patch:subject <ingen:/main/sampler> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://lv2plug.in/plugins/eg-sampler> + ] . + +<msg2> + a patch:Put ; + patch:subject <ingen:/main/out> ; + patch:body [ + a lv2:OutputPort , + lv2:AudioPort + ] . + +<msg3> + a patch:Put ; + patch:subject <ingen:/main/in> ; + patch:body [ + a lv2:InputPort , + lv2:AudioPort + ] . + +<msg4> + a patch:Put ; + patch:subject <ingen:/main/control> ; + patch:body [ + a lv2:InputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence + ] . + +<msg5> + a patch:Put ; + patch:subject <ingen:/main/gain> ; + patch:body [ + a lv2:InputPort , + lv2:ControlPort + ] . + +<msg6> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/amp/out> ; + ingen:head <ingen:/main/out> + ] . + +<msg7> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/in> ; + ingen:head <ingen:/main/amp/in> + ] . + +<msg8> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/gain> ; + ingen:head <ingen:/main/amp/gain> + ] . + +<msg9> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/control> ; + ingen:head <ingen:/main/sampler/control> + ] . + +<msg10> + a patch:Delete ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/amp/out> ; + ingen:head <ingen:/main/out> + ] . + +<msg11> + a patch:Delete ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/in> ; + ingen:head <ingen:/main/amp/in> + ] . diff --git a/tests/connect_disconnect_patch_patch.ttl b/tests/connect_disconnect_patch_patch.ttl new file mode 100644 index 00000000..b35a4b55 --- /dev/null +++ b/tests/connect_disconnect_patch_patch.ttl @@ -0,0 +1,36 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/in> ; + patch:body [ + a lv2:InputPort , + lv2:AudioPort + ] . + +<msg1> + a patch:Put ; + patch:subject <ingen:/main/out> ; + patch:body [ + a lv2:InputPort , + lv2:AudioPort + ] . + +<msg2> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/in> ; + ingen:head <ingen:/main/out> + ] . + +<msg3> + a patch:Delete ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/in> ; + ingen:head <ingen:/main/out> + ] . diff --git a/tests/copy_node.ttl b/tests/copy_node.ttl new file mode 100644 index 00000000..129ba758 --- /dev/null +++ b/tests/copy_node.ttl @@ -0,0 +1,16 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/node> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg1> + a patch:Copy ; + patch:subject <ingen:/main/node> ; + patch:destination <ingen:/main/copy> . diff --git a/tests/create_delete_node.ttl b/tests/create_delete_node.ttl new file mode 100644 index 00000000..81086e69 --- /dev/null +++ b/tests/create_delete_node.ttl @@ -0,0 +1,27 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/node> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg1> + a patch:Delete ; + patch:subject <ingen:/main/node> . + +<msg2> + a patch:Put ; + patch:subject <ingen:/main/deprecatednode> ; + patch:body [ + a ingen:Block ; + ingen:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg3> + a patch:Delete ; + patch:subject <ingen:/main/deprecatednode> . diff --git a/tests/create_delete_patch.ttl b/tests/create_delete_patch.ttl new file mode 100644 index 00000000..2fa72c42 --- /dev/null +++ b/tests/create_delete_patch.ttl @@ -0,0 +1,14 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/sub> ; + patch:body [ + a ingen:Graph ; + ] . + +<msg1> + a patch:Delete ; + patch:subject <ingen:/main/sub> . diff --git a/tests/create_delete_poly_patch.ttl b/tests/create_delete_poly_patch.ttl new file mode 100644 index 00000000..ea0228d4 --- /dev/null +++ b/tests/create_delete_poly_patch.ttl @@ -0,0 +1,15 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/sub> ; + patch:body [ + a ingen:Graph ; + ingen:polyphony 8 ; + ] . + +<msg1> + a patch:Delete ; + patch:subject <ingen:/main/sub> . diff --git a/tests/create_delete_port.ttl b/tests/create_delete_port.ttl new file mode 100644 index 00000000..ba26560d --- /dev/null +++ b/tests/create_delete_port.ttl @@ -0,0 +1,53 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/audio_in> ; + patch:body [ + a lv2:InputPort , + lv2:AudioPort + ] . + +<msg1> + a patch:Put ; + patch:subject <ingen:/main/float_in> ; + patch:body [ + a lv2:InputPort , + lv2:ControlPort + ] . + +<msg2> + a patch:Put ; + patch:subject <ingen:/main/cv_in> ; + patch:body [ + a lv2:InputPort , + lv2:CVPort + ] . + +<msg3> + a patch:Put ; + patch:subject <ingen:/main/atom_in> ; + patch:body [ + a lv2:InputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence + ] . + +<msg4> + a patch:Delete ; + patch:subject <ingen:/main/audio_in> . + +<msg5> + a patch:Delete ; + patch:subject <ingen:/main/float_in> . + +<msg6> + a patch:Delete ; + patch:subject <ingen:/main/cv_in> . + +<msg7> + a patch:Delete ; + patch:subject <ingen:/main/atom_in> . diff --git a/tests/disconnect_all_node.ttl b/tests/disconnect_all_node.ttl new file mode 100644 index 00000000..2b65f758 --- /dev/null +++ b/tests/disconnect_all_node.ttl @@ -0,0 +1,45 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/node1> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg1> + a patch:Put ; + patch:subject <ingen:/main/node2> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg2> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/node1/left_out> ; + ingen:head <ingen:/main/node2/left_in> + ] . + +<msg3> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/node1/right_out> ; + ingen:head <ingen:/main/node2/right_in> + ] . + +<msg4> + a patch:Delete ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:incidentTo <ingen:/main/node1> + ] . diff --git a/tests/disconnect_all_port.ttl b/tests/disconnect_all_port.ttl new file mode 100644 index 00000000..5c2d92f2 --- /dev/null +++ b/tests/disconnect_all_port.ttl @@ -0,0 +1,32 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/audio_in> ; + patch:body [ + a lv2:InputPort , + lv2:AudioPort + ] . + +<msg1> + a patch:Put ; + patch:subject <ingen:/main/node> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg2> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/audio_in> ; + ingen:head <ingen:/main/node/left_in> + ] . + +<msg3> + a patch:Delete ; + patch:subject <ingen:/main/audio_in> . diff --git a/tests/duplicate_node.ttl b/tests/duplicate_node.ttl new file mode 100644 index 00000000..47d71dfc --- /dev/null +++ b/tests/duplicate_node.ttl @@ -0,0 +1,19 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/node> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg1> + a patch:Put ; + patch:subject <ingen:/main/deprecatednode> ; + patch:body [ + a ingen:Block ; + lv2:prototype <ingen:/main/node> + ] . diff --git a/tests/empty.ingen/main.ttl b/tests/empty.ingen/main.ttl new file mode 100644 index 00000000..eee1ce4b --- /dev/null +++ b/tests/empty.ingen/main.ttl @@ -0,0 +1,52 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<control> + ingen:canvasX 32.0 ; + ingen:canvasY 32.0 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:designation lv2:control ; + lv2:index 0 ; + lv2:name "Control" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "control" ; + a atom:AtomPort , + lv2:InputPort . + +<> + ingen:polyphony 1 ; + <http://lv2plug.in/ns/extensions/ui#ui> ingen:GraphUIGtk2 ; + lv2:extensionData <http://lv2plug.in/ns/ext/state#interface> ; + lv2:port <control> , + <notify> ; + lv2:symbol "empty" ; + doap:name "empty" ; + a ingen:Graph , + lv2:Plugin . + +<notify> + ingen:canvasX 128.0 ; + ingen:canvasY 32.0 ; + ingen:polyphonic false ; + atom:bufferType atom:Sequence ; + atom:supports patch:Message ; + <http://lv2plug.in/ns/ext/resize-port#minimumSize> 4096 ; + lv2:designation lv2:control ; + lv2:index 1 ; + lv2:name "Control" ; + lv2:portProperty lv2:connectionOptional ; + lv2:symbol "notify" ; + a atom:AtomPort , + lv2:OutputPort . + diff --git a/tests/empty.ingen/manifest.ttl b/tests/empty.ingen/manifest.ttl new file mode 100644 index 00000000..71f194a0 --- /dev/null +++ b/tests/empty.ingen/manifest.ttl @@ -0,0 +1,16 @@ +@prefix atom: <http://lv2plug.in/ns/ext/atom#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix doap: <http://usefulinc.com/ns/doap#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix midi: <http://lv2plug.in/ns/ext/midi#> . +@prefix owl: <http://www.w3.org/2002/07/owl#> . +@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . +@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . +@prefix xsd: <http://www.w3.org/2001/XMLSchema#> . + +<main.ttl> + lv2:prototype ingen:GraphPrototype ; + a ingen:Graph , + lv2:Plugin ; + rdfs:seeAlso <main.ttl> . diff --git a/tests/enable_graph.ttl b/tests/enable_graph.ttl new file mode 100644 index 00000000..f3fa786c --- /dev/null +++ b/tests/enable_graph.ttl @@ -0,0 +1,15 @@ +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . + +<msg0> + a patch:Set ; + patch:subject <ingen:/main> ; + patch:property ingen:enabled ; + patch:value true . + +<msg1> + a patch:Set ; + patch:subject <ingen:/main> ; + patch:property ingen:enabled ; + patch:value false . diff --git a/tests/get_engine.ttl b/tests/get_engine.ttl new file mode 100644 index 00000000..adfa6a01 --- /dev/null +++ b/tests/get_engine.ttl @@ -0,0 +1,7 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Get ; + patch:subject <ingen:/engine> . diff --git a/tests/get_node.ttl b/tests/get_node.ttl new file mode 100644 index 00000000..12977092 --- /dev/null +++ b/tests/get_node.ttl @@ -0,0 +1,15 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/node> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg1> + a patch:Get ; + patch:subject <ingen:/main/node> . diff --git a/tests/get_patch.ttl b/tests/get_patch.ttl new file mode 100644 index 00000000..9ea9e036 --- /dev/null +++ b/tests/get_patch.ttl @@ -0,0 +1,39 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/sub> ; + patch:body [ + a ingen:Graph + ] . + +<msg1> + a patch:Put ; + patch:subject <ingen:/main/sub/node1> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg2> + a patch:Put ; + patch:subject <ingen:/main/sub/node2> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Combo> + ] . + +<msg3> + a patch:Put ; + patch:subject <ingen:/main/> ; + patch:body [ + a ingen:Arc ; + ingen:tail <ingen:/main/sub/node1/left_out> ; + ingen:head <ingen:/main/sub/node2/left_in> + ] . + +<msg4> + a patch:Get ; + patch:subject <ingen:/main/> . diff --git a/tests/get_plugin.ttl b/tests/get_plugin.ttl new file mode 100644 index 00000000..19a9c93c --- /dev/null +++ b/tests/get_plugin.ttl @@ -0,0 +1,7 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Get ; + patch:subject <http://lv2plug.in/plugins/eg-amp> . diff --git a/tests/get_plugins.ttl b/tests/get_plugins.ttl new file mode 100644 index 00000000..8dc91cdf --- /dev/null +++ b/tests/get_plugins.ttl @@ -0,0 +1,7 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Get ; + patch:subject <ingen:/plugins> . diff --git a/tests/get_port.ttl b/tests/get_port.ttl new file mode 100644 index 00000000..4f26e499 --- /dev/null +++ b/tests/get_port.ttl @@ -0,0 +1,15 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/in> ; + patch:body [ + a lv2:InputPort , + lv2:AudioPort + ] . + +<msg1> + a patch:Get ; + patch:subject <ingen:/main/in> . diff --git a/tests/ingen_bench.cpp b/tests/ingen_bench.cpp new file mode 100644 index 00000000..e8cccf4c --- /dev/null +++ b/tests/ingen_bench.cpp @@ -0,0 +1,140 @@ +/* + 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 <cstdlib> +#include <iostream> +#include <string> +#include <thread> + +#include "ingen/Clock.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Forge.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Parser.hpp" +#include "ingen/World.hpp" +#include "ingen/runtime_paths.hpp" +#include "ingen/types.hpp" + +#include "TestClient.hpp" +#include "ingen_config.h" + +using namespace std; +using namespace Ingen; + +World* world = nullptr; + +static void +ingen_try(bool cond, const char* msg) +{ + if (!cond) { + cerr << "ingen: Error: " << msg << endl; + delete world; + exit(EXIT_FAILURE); + } +} + +static std::string +real_path(const char* path) +{ + char* const c_real_path = realpath(path, nullptr); + const std::string result(c_real_path ? c_real_path : ""); + free(c_real_path); + return result; +} + +int +main(int argc, char** argv) +{ + set_bundle_path_from_code((void*)&ingen_try); + + // Create world + try { + world = new World(nullptr, nullptr, nullptr); + world->conf().add( + "output", "output", 'O', "File to write benchmark output", + Ingen::Configuration::SESSION, world->forge().String, Atom()); + world->load_configuration(argc, argv); + } catch (std::exception& e) { + cout << "ingen: " << e.what() << endl; + return EXIT_FAILURE; + } + + // Get mandatory command line arguments + const Atom& load = world->conf().option("load"); + const Atom& out = world->conf().option("output"); + if (!load.is_valid() || !out.is_valid()) { + cerr << "Usage: ingen_bench --load START_GRAPH --output OUT_FILE" << endl; + return EXIT_FAILURE; + } + + // Get start graph and output file options + const std::string start_graph = real_path((const char*)load.get_body()); + const std::string out_file = (const char*)out.get_body(); + if (start_graph.empty()) { + cerr << "error: initial graph '" + << ((const char*)load.get_body()) + << "' does not exist" << endl; + return EXIT_FAILURE; + } + + // Load modules + ingen_try(world->load_module("server"), + "Unable to load server module"); + + // Initialise engine + ingen_try(bool(world->engine()), + "Unable to create engine"); + world->engine()->init(48000.0, 4096, 4096); + world->engine()->activate(); + + // Load graph + if (!world->parser()->parse_file(world, world->interface().get(), start_graph)) { + cerr << "error: failed to load initial graph " << start_graph << endl; + return EXIT_FAILURE; + } + world->engine()->flush_events(std::chrono::milliseconds(20)); + + // Run benchmark + // TODO: Set up real-time scheduling for this and worker threads + Ingen::Clock clock; + const uint32_t n_test_frames = 1 << 20; + const uint32_t block_length = 4096; + const uint64_t t_start = clock.now_microseconds(); + for (uint32_t i = 0; i < n_test_frames; i += block_length) { + world->engine()->advance(block_length); + world->engine()->run(block_length); + //world->engine()->main_iteration(); + } + const uint64_t t_end = clock.now_microseconds(); + + // Write log output + FILE* log = fopen(out_file.c_str(), "a"); + if (ftell(log) == 0) { + fprintf(log, "# n_threads\trun_time\treal_time\n"); + } + fprintf(log, "%u\t%f\t%f\n", + world->conf().option("threads").get<int32_t>(), + (t_end - t_start) / 1000000.0, + (n_test_frames / 48000.0)); + fclose(log); + + // Shut down + world->engine()->deactivate(); + + delete world; + return EXIT_SUCCESS; +} diff --git a/tests/ingen_test.cpp b/tests/ingen_test.cpp new file mode 100644 index 00000000..c0a7bd32 --- /dev/null +++ b/tests/ingen_test.cpp @@ -0,0 +1,223 @@ +/* + 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 <iostream> +#include <string> + +#include <boost/optional.hpp> + +#include "raul/Path.hpp" + +#include "serd/serd.h" +#include "sord/sordmm.hpp" +#include "sratom/sratom.h" + +#include "ingen_config.h" + +#include "ingen/AtomReader.hpp" +#include "ingen/AtomWriter.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/Configuration.hpp" +#include "ingen/EngineBase.hpp" +#include "ingen/Interface.hpp" +#include "ingen/Parser.hpp" +#include "ingen/Properties.hpp" +#include "ingen/Serialiser.hpp" +#include "ingen/Store.hpp" +#include "ingen/URIMap.hpp" +#include "ingen/World.hpp" +#include "ingen/filesystem.hpp" +#include "ingen/runtime_paths.hpp" +#include "ingen/types.hpp" + +#include "TestClient.hpp" + +using namespace std; +using namespace Ingen; + +World* world = nullptr; + +static void +ingen_try(bool cond, const char* msg) +{ + if (!cond) { + cerr << "ingen: Error: " << msg << endl; + delete world; + exit(EXIT_FAILURE); + } +} + +int +main(int argc, char** argv) +{ + set_bundle_path_from_code((void*)&ingen_try); + + // Create world + try { + world = new World(nullptr, nullptr, nullptr); + world->load_configuration(argc, argv); + } catch (std::exception& e) { + cout << "ingen: " << e.what() << endl; + return EXIT_FAILURE; + } + + // Get mandatory command line arguments + const Atom& load = world->conf().option("load"); + const Atom& execute = world->conf().option("execute"); + if (!load.is_valid() || !execute.is_valid()) { + cerr << "Usage: ingen_test --load START_GRAPH --execute COMMANDS_FILE" << endl; + return EXIT_FAILURE; + } + + // Get start graph and commands file options + const char* load_path = (const char*)load.get_body(); + char* real_start_graph = realpath(load_path, nullptr); + if (!real_start_graph) { + cerr << "error: initial graph '" << load_path << "' does not exist" << endl; + return EXIT_FAILURE; + } + + const std::string start_graph = real_start_graph; + const FilePath cmds_file_path = (const char*)execute.get_body(); + free(real_start_graph); + + // Load modules + ingen_try(world->load_module("server"), + "Unable to load server module"); + + // Initialise engine + ingen_try(bool(world->engine()), + "Unable to create engine"); + world->engine()->init(48000.0, 4096, 4096); + world->engine()->activate(); + + // Load graph + if (!world->parser()->parse_file(world, world->interface().get(), start_graph)) { + cerr << "error: failed to load initial graph " << start_graph << endl; + return EXIT_FAILURE; + } + world->engine()->flush_events(std::chrono::milliseconds(20)); + + // Read commands + + LV2_URID_Map* map = &world->uri_map().urid_map_feature()->urid_map; + Sratom* sratom = sratom_new(map); + + sratom_set_object_mode(sratom, SRATOM_OBJECT_MODE_BLANK_SUBJECT); + + LV2_Atom_Forge forge; + lv2_atom_forge_init(&forge, map); + + AtomForgeSink out(&forge); + + // AtomReader to read commands from a file and send them to engine + AtomReader atom_reader(world->uri_map(), + world->uris(), + world->log(), + *world->interface().get()); + + // AtomWriter to serialise responses from the engine + SPtr<Interface> client(new TestClient(world->log())); + + world->interface()->set_respondee(client); + world->engine()->register_client(client); + + SerdURI cmds_base; + SerdNode cmds_file_uri = serd_node_new_file_uri( + (const uint8_t*)cmds_file_path.c_str(), + nullptr, &cmds_base, true); + Sord::Model* cmds = new Sord::Model(*world->rdf_world(), + (const char*)cmds_file_uri.buf); + SerdEnv* env = serd_env_new(&cmds_file_uri); + cmds->load_file(env, SERD_TURTLE, cmds_file_path); + Sord::Node nil; + int n_events = 0; + for (;; ++n_events) { + std::string subject_str = (fmt("msg%1%") % n_events).str(); + Sord::URI subject(*world->rdf_world(), subject_str, + (const char*)cmds_file_uri.buf); + Sord::Iter iter = cmds->find(subject, nil, nil); + if (iter.end()) { + break; + } + + out.clear(); + sratom_read(sratom, &forge, world->rdf_world()->c_obj(), + cmds->c_obj(), subject.c_obj()); + +#if 0 + const LV2_Atom* atom = out.atom(); + cerr << "READ " << atom->size << " BYTES" << endl; + cerr << sratom_to_turtle( + sratom, + &world->uri_map().urid_unmap_feature()->urid_unmap, + (const char*)cmds_file_uri.buf, + NULL, NULL, atom->type, atom->size, LV2_ATOM_BODY(atom)) << endl; +#endif + + if (!atom_reader.write(out.atom(), n_events + 1)) { + return EXIT_FAILURE; + } + + world->engine()->flush_events(std::chrono::milliseconds(20)); + } + + delete cmds; + + // Save resulting graph + auto r = world->store()->find(Raul::Path("/")); + const std::string base = cmds_file_path.stem(); + const std::string out_name = base.substr(0, base.find('.')) + ".out.ingen"; + const FilePath out_path = filesystem::current_path() / out_name; + world->serialiser()->write_bundle(r->second, URI(out_path)); + + // Undo every event (should result in a graph identical to the original) + for (int i = 0; i < n_events; ++i) { + world->interface()->undo(); + world->engine()->flush_events(std::chrono::milliseconds(20)); + } + + // Save completely undone graph + r = world->store()->find(Raul::Path("/")); + const std::string undo_name = base.substr(0, base.find('.')) + ".undo.ingen"; + const FilePath undo_path = filesystem::current_path() / undo_name; + world->serialiser()->write_bundle(r->second, URI(undo_path)); + + // Redo every event (should result in a graph identical to the pre-undo output) + for (int i = 0; i < n_events; ++i) { + world->interface()->redo(); + world->engine()->flush_events(std::chrono::milliseconds(20)); + } + + // Save completely redone graph + r = world->store()->find(Raul::Path("/")); + const std::string redo_name = base.substr(0, base.find('.')) + ".redo.ingen"; + const FilePath redo_path = filesystem::current_path() / redo_name; + world->serialiser()->write_bundle(r->second, URI(redo_path)); + + serd_env_free(env); + sratom_free(sratom); + serd_node_free(&cmds_file_uri); + + // Shut down + world->engine()->deactivate(); + + delete world; + return EXIT_SUCCESS; +} diff --git a/tests/load_graph.ttl b/tests/load_graph.ttl new file mode 100644 index 00000000..f1cd9dda --- /dev/null +++ b/tests/load_graph.ttl @@ -0,0 +1,8 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Copy ; + patch:subject <empty.ingen/> ; + patch:destination <ingen:/main/> . diff --git a/tests/move_node.ttl b/tests/move_node.ttl new file mode 100644 index 00000000..22f6cfb8 --- /dev/null +++ b/tests/move_node.ttl @@ -0,0 +1,16 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/node> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg1> + a patch:Move ; + patch:subject <ingen:/main/node> ; + patch:destination <ingen:/main/tone> . diff --git a/tests/move_port.ttl b/tests/move_port.ttl new file mode 100644 index 00000000..4b035037 --- /dev/null +++ b/tests/move_port.ttl @@ -0,0 +1,16 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/in> ; + patch:body [ + a lv2:InputPort , + lv2:AudioPort + ] . + +<msg1> + a patch:Move ; + patch:subject <ingen:/main/in> ; + patch:destination <ingen:/main/input> . diff --git a/tests/move_root_port.ttl b/tests/move_root_port.ttl new file mode 100644 index 00000000..2c925767 --- /dev/null +++ b/tests/move_root_port.ttl @@ -0,0 +1,20 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/in> ; + patch:body [ + a lv2:InputPort , + lv2:AudioPort + ] . + +<msg1> + a patch:Move ; + patch:subject <ingen:/main/in> ; + patch:destination <ingen:/main/input> . + +<msg2> + a patch:Delete ; + patch:subject <ingen:/main/input> . diff --git a/tests/poly.ttl b/tests/poly.ttl new file mode 100644 index 00000000..a02ba96e --- /dev/null +++ b/tests/poly.ttl @@ -0,0 +1,25 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/node> ; + patch:body [ + a ingen:Block ; + lv2:prototype <http://drobilla.net/plugins/mda/Shepard> + ] . + +<msg1> + a patch:Set ; + patch:context ingen:internalContext ; + patch:subject <ingen:/main/> ; + patch:property ingen:polyphony ; + patch:value 4 . + +<msg2> + a patch:Set ; + patch:context ingen:externalContext ; + patch:subject <ingen:/main/node> ; + patch:property ingen:polyphonic ; + patch:value true . diff --git a/tests/put_audio_in.ttl b/tests/put_audio_in.ttl new file mode 100644 index 00000000..97468cff --- /dev/null +++ b/tests/put_audio_in.ttl @@ -0,0 +1,10 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/in> ; + patch:body [ + a lv2:InputPort , + lv2:AudioPort + ] . diff --git a/tests/save_graph.ttl b/tests/save_graph.ttl new file mode 100644 index 00000000..5f472d80 --- /dev/null +++ b/tests/save_graph.ttl @@ -0,0 +1,8 @@ +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . +@prefix ingen: <http://drobilla.net/ns/ingen#> . + +<msg0> + a patch:Copy ; + patch:subject <ingen:/main/> ; + patch:destination <file:///tmp/savetest.ingen/> . diff --git a/tests/set_graph_poly.ttl b/tests/set_graph_poly.ttl new file mode 100644 index 00000000..0933c3a4 --- /dev/null +++ b/tests/set_graph_poly.ttl @@ -0,0 +1,17 @@ +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . + +<msg0> + a patch:Set ; + patch:context ingen:internalContext ; + patch:subject <ingen:/main/> ; + patch:property ingen:polyphony ; + patch:value 4 . + +<msg1> + a patch:Set ; + patch:context ingen:internalContext ; + patch:subject <ingen:/main/> ; + patch:property ingen:polyphony ; + patch:value 1 . diff --git a/tests/set_patch_port_value.ttl b/tests/set_patch_port_value.ttl new file mode 100644 index 00000000..07b467b4 --- /dev/null +++ b/tests/set_patch_port_value.ttl @@ -0,0 +1,17 @@ +@prefix ingen: <http://drobilla.net/ns/ingen#> . +@prefix lv2: <http://lv2plug.in/ns/lv2core#> . +@prefix patch: <http://lv2plug.in/ns/ext/patch#> . + +<msg0> + a patch:Put ; + patch:subject <ingen:/main/in> ; + patch:body [ + a lv2:InputPort , + lv2:ControlPort + ] . + +<msg1> + a patch:Set ; + patch:subject <ingen:/main/in> ; + patch:property ingen:value ; + patch:value 0.5 .
\ No newline at end of file diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp new file mode 100644 index 00000000..a0cc53ac --- /dev/null +++ b/tests/test_utils.hpp @@ -0,0 +1,40 @@ +/* + 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 <iostream> + +#include <boost/format.hpp> + +typedef boost::basic_format<char> fmt; + +#define EXPECT_TRUE(value) \ + if (!(value)) { \ + std::cerr << (fmt("error: %1%:%2%: !%3%\n") % __FILE__ % \ + __LINE__ % (#value)); \ + } + +#define EXPECT_FALSE(value) \ + if ((value)) { \ + std::cerr << (fmt("error: %1%:%2%: !%3%\n") % __FILE__ % \ + __LINE__ % (#value)); \ + } + +#define EXPECT_EQ(value, expected) \ + if (!((value) == (expected))) { \ + std::cerr << (fmt("error: %1%:%2%: %3% != %4%\n") % __FILE__ % \ + __LINE__ % (#value) % (#expected)); \ + std::cerr << "note: actual value: " << value << std::endl; \ + } diff --git a/tests/tst_FilePath.cpp b/tests/tst_FilePath.cpp new file mode 100644 index 00000000..55d6f1c2 --- /dev/null +++ b/tests/tst_FilePath.cpp @@ -0,0 +1,103 @@ +/* + 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 <boost/utility/string_view.hpp> + +#include "ingen/FilePath.hpp" +#include "test_utils.hpp" + +using Ingen::FilePath; + +int +main(int, char**) +{ + EXPECT_EQ(FilePath("/").parent_path(), FilePath("/")); + + EXPECT_TRUE(FilePath("/abs").is_absolute()) + EXPECT_FALSE(FilePath("/abs").is_relative()) + EXPECT_EQ(FilePath("/abs").root_name(), FilePath()); + EXPECT_EQ(FilePath("/abs").root_directory(), FilePath("/")); + EXPECT_EQ(FilePath("/abs").root_path(), FilePath("/")); + EXPECT_EQ(FilePath("/abs").relative_path(), FilePath("abs")); + EXPECT_EQ(FilePath("/abs").parent_path(), FilePath("/")); + EXPECT_EQ(FilePath("/abs").filename(), FilePath("abs")); + EXPECT_EQ(FilePath("/abs").stem(), FilePath("abs")); + EXPECT_EQ(FilePath("/abs").extension(), FilePath()); + + EXPECT_FALSE(FilePath("rel").is_absolute()) + EXPECT_TRUE(FilePath("rel").is_relative()) + EXPECT_EQ(FilePath("rel").root_name(), FilePath()); + EXPECT_EQ(FilePath("rel").root_directory(), FilePath()); + EXPECT_EQ(FilePath("rel").root_path(), FilePath()); + EXPECT_EQ(FilePath("rel").relative_path(), FilePath()); + EXPECT_EQ(FilePath("rel").parent_path(), FilePath()); + EXPECT_EQ(FilePath("rel").filename(), "rel"); + EXPECT_EQ(FilePath("rel").stem(), "rel"); + EXPECT_EQ(FilePath("rel").extension(), FilePath()); + + EXPECT_FALSE(FilePath("file.txt").is_absolute()) + EXPECT_TRUE(FilePath("file.txt").is_relative()) + EXPECT_EQ(FilePath("file.txt").filename(), "file.txt"); + EXPECT_EQ(FilePath("file.txt").stem(), "file"); + EXPECT_EQ(FilePath("file.txt").extension(), ".txt"); + + EXPECT_TRUE(FilePath("/abs/file.txt").is_absolute()) + EXPECT_FALSE(FilePath("/abs/file.txt").is_relative()) + EXPECT_EQ(FilePath("/abs/file.txt").filename(), "file.txt"); + EXPECT_EQ(FilePath("/abs/file.txt").stem(), "file"); + EXPECT_EQ(FilePath("/abs/file.txt").extension(), ".txt"); + + EXPECT_FALSE(FilePath("rel/file.txt").is_absolute()) + EXPECT_TRUE(FilePath("rel/file.txt").is_relative()) + EXPECT_EQ(FilePath("rel/file.txt").filename(), "file.txt"); + EXPECT_EQ(FilePath("rel/file.txt").stem(), "file"); + EXPECT_EQ(FilePath("rel/file.txt").extension(), ".txt"); + + FilePath path("/x"); + EXPECT_EQ(path, "/x"); + path = std::string("/a"); + EXPECT_EQ(path, "/a"); + + path /= FilePath("b"); + EXPECT_EQ(path, "/a/b"); + + path += FilePath("ar"); + EXPECT_EQ(path, "/a/bar"); + + path += std::string("/c"); + EXPECT_EQ(path, "/a/bar/c"); + + path += "a"; + EXPECT_EQ(path, "/a/bar/ca"); + + path += 'r'; + EXPECT_EQ(path, "/a/bar/car"); + + path += boost::string_view("/d"); + EXPECT_EQ(path, "/a/bar/car/d"); + + const FilePath apple("apple"); + const FilePath zebra("zebra"); + EXPECT_TRUE(apple == apple); + EXPECT_TRUE(apple != zebra); + EXPECT_TRUE(apple < zebra); + EXPECT_TRUE(apple <= zebra); + EXPECT_TRUE(apple <= apple); + EXPECT_TRUE(zebra > apple); + EXPECT_TRUE(zebra >= apple); + EXPECT_TRUE(zebra >= zebra); + return 0; +} @@ -1,16 +1,169 @@ #!/usr/bin/env python +# encoding: latin-1 +# Thomas Nagy, 2005-2017 +# +""" +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: -# Minimal waf script for projects that include waflib directly +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. -from waflib import Context, Scripting +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. -import inspect -import os +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. -def main(): - script_path = os.path.abspath(inspect.getfile(inspect.getmodule(main))) - project_path = os.path.dirname(script_path) - Scripting.waf_entry_point(os.getcwd(), Context.WAFVERSION, project_path) +THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +""" + +import os, sys, inspect + +VERSION="2.0.4" +REVISION="0466fbd730f20d94d655ebe324c8478e" +GIT="x" +INSTALL='' +C1='#1' +C2='#/' +C3='#.' +cwd = os.getcwd() +join = os.path.join + + +WAF='waf' +def b(x): + return x +if sys.hexversion>0x300000f: + WAF='waf3' + def b(x): + return x.encode() + +def err(m): + print(('\033[91mError: %s\033[0m' % m)) + sys.exit(1) + +def unpack_wafdir(dir, src): + f = open(src,'rb') + c = 'corrupt archive (%d)' + while 1: + line = f.readline() + if not line: err('run waf-light from a folder containing waflib') + if line == b('#==>\n'): + txt = f.readline() + if not txt: err(c % 1) + if f.readline() != b('#<==\n'): err(c % 2) + break + if not txt: err(c % 3) + txt = txt[1:-1].replace(b(C1), b('\n')).replace(b(C2), b('\r')).replace(b(C3), b('\x00')) + + import shutil, tarfile + try: shutil.rmtree(dir) + except OSError: pass + try: + for x in ('Tools', 'extras'): + os.makedirs(join(dir, 'waflib', x)) + except OSError: + err("Cannot unpack waf lib into %s\nMove waf in a writable directory" % dir) + + os.chdir(dir) + tmp = 't.bz2' + t = open(tmp,'wb') + try: t.write(txt) + finally: t.close() + + try: + t = tarfile.open(tmp) + except: + try: + os.system('bunzip2 t.bz2') + t = tarfile.open('t') + tmp = 't' + except: + os.chdir(cwd) + try: shutil.rmtree(dir) + except OSError: pass + err("Waf cannot be unpacked, check that bzip2 support is present") + + try: + for x in t: t.extract(x) + finally: + t.close() + + for x in ('Tools', 'extras'): + os.chmod(join('waflib',x), 493) + + if sys.hexversion<0x300000f: + sys.path = [join(dir, 'waflib')] + sys.path + import fixpy2 + fixpy2.fixdir(dir) + + os.remove(tmp) + os.chdir(cwd) + + try: dir = unicode(dir, 'mbcs') + except: pass + try: + from ctypes import windll + windll.kernel32.SetFileAttributesW(dir, 2) + except: + pass + +def test(dir): + try: + os.stat(join(dir, 'waflib')) + return os.path.abspath(dir) + except OSError: + pass + +def find_lib(): + src = os.path.abspath(inspect.getfile(inspect.getmodule(err))) + base, name = os.path.split(src) + + #devs use $WAFDIR + w=test(os.environ.get('WAFDIR', '')) + if w: return w + + #waf-light + if name.endswith('waf-light'): + w = test(base) + if w: return w + err('waf-light requires waflib -> export WAFDIR=/folder') + + dirname = '%s-%s-%s' % (WAF, VERSION, REVISION) + for i in (INSTALL,'/usr','/usr/local','/opt'): + w = test(i + '/lib/' + dirname) + if w: return w + + #waf-local + dir = join(base, (sys.platform != 'win32' and '.' or '') + dirname) + w = test(dir) + if w: return w + + #unpack + unpack_wafdir(dir, src) + return dir + +wafdir = find_lib() +sys.path.insert(0, wafdir) if __name__ == '__main__': - main() + + from waflib import Scripting + Scripting.waf_entry_point(cwd, VERSION, wafdir) + +#==> +#BZh91AY&SYhë+÷¥Õÿÿÿ°ÐÿÿÿÿÿÿÿÿÿÿÿE ‚„ 0Á#.ˆ¨bJ¼wzÛ¢#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.Ož¦ÕµfXj™NìRíP¥mªd¾ÝØ©´ÞíÛ`¥<¶µS¶o.ws²šÖå}íJæÍMlonƧׅï.s6fÓ裈®µ½Í»¯XVfo7£ÕQØoZ¹èÅ•Þû1qlõ«ïnß]wÃîóÞÝÍ7[E†º{;·z÷ß>ùðîÀ}»Þ¾>+j{½í}Oî÷;SVÖ{íà#.#.#.½€ôØЀt¯³p>€Ÿo#.››0í }Þƒßes}ìwvtP=#.hÖq·{¶Ù¨»A@kÐh(#1ÐÐÙÜURUR”¡J/{ƒD’K¬AE#.²’¡èÖöàBÏ]ëîÝ{„}<óíÙÝ9;¾¼Ú²•“»F·cÙ:dS“4Íñmæè×M sKåU«ß.®Æª\ôïM:ï{I™—{n›w¶;}|Ž™U·Ÿg¯3>ã¹÷»Êíê÷ŽÜgk];±C]ížÊÐaQU$%ZkM%í»3¬…±Ý]Öò{¯rÇmìié¡wvÆvàôn›mB¥Ï{Ùo4#.P Hìez`(ô={#.‘§B”÷½¹ïXâQ{nî5vkO¼5ï¾úû¾fÝ}/cÝ€]öÓæûŒ‹[}uÛ4úã#.ozîlÆÀ7ÇkÎ÷ ž÷kÐ#1ûuݱ77+ëë×¥=΃£Ž½WqÝh°öÇ@#f}ÜpZ{»wruÎí®e¶|6órùöúñJæ§s¹>ï<÷,íNãkO·igŸyÝæ·w.ëŽío¼{ǦßéÝó•ÛQ¶•(ž¹»vû…³ZÆÞOZˆËÛ:ÓÁžAM¯\y;ÙðÔ>ÜíÃ\Ù|û©î{§±Û7xì¾o®³^Ñ`ûÏyov_o«Ùæ-¾µ¶=µpöWvØ× mum®îž{X#.o]M½ç½==÷»®áî÷g{k¦ïH(¡@%RDITu›ÍÛvŽšë˜k£QmiÛ})î—F›°à›™¶îÖöÝ–‘§Q+®Ì¯7ŽôÕEwW{¶Q ”äw¹ß{{yÏ€#.}÷ÏQò#.#.S|ã×6\rdzÞy¤]ª=Û}ÏTôezcw>÷®ÙgmÝKÒ39™uè|wsÓèç³uJOnیعÉß^:ö¹iòwî½7s®Ôð•hÉ¢œ¶öºUâf¥;kp˾î£äûàÓÜgkª>ÛÄûµ·Ö¹µfd3bñÚ'#/¾ÝϨ¹¥Î]|]Ùäì»”ÆíÇ3Ö†#/˜fš ½Ü+™˜Km»·Ønìw@Ø•Nx7KÕÛ¯^wËÞÝ{Ý¥Í×»X#.€U[Û]EU;c¥Þ÷žÏCMÀÃ.·zíiÜ^¼pçÓËÞ}ç£Ï\ûíFûîÜÝwÜ+M—°œ¾ûë¼ú³5èÙ õ zav5O^ÝaYÝ;ÎK`Áà°.Ü©Õ`çCœì÷ãÇß>﮳<ÛCOAÐVC×»[î¥ØtØ{%[ÓnñóÌûÞöð¶Ð#.#.(mÞ˸ï{‰¶õ¥Å¼Ngwvëc*öéÊ]eËïrŽš{²œíÒU#1í® ]7l&Ý×Þg-‰žéÜvªÅ¼®õëɺî×kǾŸ÷>³Ý÷´õñ4}é.æ@ÚßGÎŒTlÈ-™ï»Ã±âàвæ÷gs¶ë=iÞ;¨m¾O}÷;ö8mw}¸zøºöó¯ñyC-O™¾†š #.Ð#.4#.FM†¦Šz“OF§”LŒ†OSÔò ”Ð! &@L)¢~š§èÕ?T<šF@ÃPdh#.#.#.#.‚D! ÓA¡IOdÐÁOJhô‡©é”zG¦Ñê#.#.#.#.OT¤ˆAôž jh†i5=MSOP2#.j€2yG¨z€#.€#.IM4Ð#.C@ A4Âz™=MLÔÄj4È#/#.@#.MDA#.&24#.§ˆž¤ý'ª~£QêzM ö©Ð#.#. #.ÿûæ?Ù«Zî%üŽ]涮æ_¯ZÒêÄF7Ƶ®éÂ2ŶS/}kmsVµFÚÈú’ÔHB?AùÿOèºýYŸ¤xé7f)Ii¼ÈDR›¯Îe<Ó¼V!<N.JlT,UÜL$FÕ‘ý²&*Þ«òCrƒ¤#.(Ducu rï|˜ˆ\¼´ÓÅÄÕ:§NªmæŠx³‰œq--W3|’m¼Þ¸âšÍöSðß[¾D(á AHÅ`F¨(QmdÛVe¤µ«¥¶¹KQF ˆ4‘Z‚Ôd+œç,!"E4+)Š BG"(¡`(6"(Bº Ñ#.l#1ºjÕÝ©ÑlV-íwmI[jªÞú¶ÕjffBÉFf SLM†–((’DšMLÄÉa5L±Ò”iE6Òc„„Í’PFCT´[ƒhÍ%“K#†Ñˆ@¢£‘6”£H¬jXЦ,bc(LØÒE&ÉhÚˆ¡,²Ò˜ÊÑTD,´†ÍÁ²“5ÖEhÀ*#/&ÈŠ¤Ø¦2P&f™iÀŠR±©J6Úm[+kAb!5C6$Ñ™’d&ˆ Ë&Úm´ÓhRQ²’¦¶2Զ̩–“)¤¢4FÀ©aÍY¢’RÆ‚–E1L„ÆЉ2¢Í"±I„±Q …ŠMH”f•‚¥1Š6!PHŒ¦0-ˆÉ#jL²ÄA3BQ"„C)%#F•”fŒˆÙE,šÉEѱ-"Ä2YHH±¬¥$&(’“#(ˆÉ¡D3#1“F‘(Æ“ k !Q‹E€¢fRÓLÀ66ÉÅ&3M3a3BÄmI° 6VJMŠÄD–ŒPRBX€‘4h"Q(,0ÊÉ)Åš(”•LÔlVÒjH’b iJ$ ‘d¶ $‹,Ê)dš&̤FÆ"M‰‘£6DÁ3lÓ!RÁeŒT†ÉšCF"5+%’6#/•!4H¥’BA5"E¦š,hµI!,Êl£(1h¦f‘´F‚)¥Ú˜,Pi,ÆB1d¤²$˜‹Ó0¢”Q£0Á¨¦…&*#/‚$H$0ŠLÆ’Fˆ‘2Âb‚6I•35f4U)TJ"fCR4Ä4ÍŠ”6ÊhÙ#Eb’I6H£S*l‹LÑcb)¤ÊLÊb–FVm2%b#ILÈSSI¦H)e¤54ZmŠ#f¥Jd˜˜Ù#&IJÉ¢ÆJÄc(ÊH&b(ØÛ’Mi(Êj6ME’H¡¤l ˆÆ –Í&&ÐhÑSY„ÒeE3!³"IM’™PLÙ2Vl¬ZÂX‰‰4Ë$IbÚ5A¬E2ØR$(ÆÅ"A„e’Å£L2™¦”Á²VSJ`¤Ö’#/ &)IŒÂ2ɲ#)‚E6Ti–¦• ‘3F‰¶ÔªÚ2†JÓ,e34¢’S1Z#/J[–h³šc(K,©²É±Z™l-š10Å6#)@d°Ò–¿™×m‘ ©ªAc´QŠl$ѤÔmd‘K)£¡£Z&–¨ØØEI5Y–FY1”d‹)€•&1´ˆ0ÄÌÒ–ÌÕŒI¥BØÚŠBÑ“L"Xʤ“23XXÚ#/+4S&L´›A±eXÛ6P¬‰–ESÍE0¦ÍŒ¤ÌÛ%´ÒÂcFÖ‹DkdÓ5’J™e6²kE6ÊRšbÓÔ$d¤mEŠVX“I©*#hªÁZ‚ÆŠÚ2TEŠØ¶“Y(¶£h¨©5bh ’*‹Q*Ñ°ËF¬Xƒi4”4BkPÂJ)ÚTLD-)&)Zk±£i‰lj’Á¢ÚH+mS[lÓJ¨HL¦jjÉ¢M‘‰”Ô"‹$VÔµ›c¦Ól³J˜Í4Ú¦D[iT²I™ªTŒY¦‰´$™&[,hM2ÒY6ÊÊ´kD²ŒaF*,H`‰˜Ðb“,,D‰Ú ŒCe´ÍS0!*(dÖ’²M,l™4Q²!²Å"›)FÈŠÍH²!bS2K#1J,Å3P¢Ñ¬ÉŒ¬Si2QÖ@¤#4È™$"50XÊ&‚SPYHAJh±Dҙřf”#/°˜Ä ²cÂRÉ2É ¨´dˆ"2ÔQF̤’Œ‚2 E”¬d0Ú*LjJ2lŒlI–(ÉXÒ$ 4iÆM&“fSfRRÌÚR’±L,‘¨†5$FÍhÍXh©M&ÃLl±¬Ê6£@‘Œ„LR#/16$©6“(QRšb¤‚Š[%2£h*™Sd²E“(LTI¤(iFJÌ’Ú+dF„S3 dJ‚ˆ0Ùf(ˆd؃Y¤lQ¢³I²ˆ›5$$-„¨¨ÔQX̶L1BJ#1F–LŒRˆ#b5e–K!¢ŒÂÉM”%Im¢¡"±ª+$Ñ°JU(6("2Fɪ-“ D0°ƒT¥#/Li†)TjˆÒšid(¢Ä™6Ì‹D‹Qc”ª¥¦Š¢ØÒ`ˆ²X’)¦R²…bÊÀ ‰’TØ”DÍ%ZˆÚÁ&*f¢¤´X”Y¥¨ÉXµ´˜¤¬Ò©mDÐ6%2”Ô–$’ÅIFÉ¢HŒIT”†¨¤Ì‚1k2‚‹)S*b)¶¤¬–É!M$Ôh"0Q6kI´[)%Š“LhÁ1`)$V¥£ie¢Ñµ‰6´U£)FÒUH‚£m’‹ŠhL’™1¦¤IA©›$ͨÔU%‹T•,Õ“Z´[¢¢‰"ÛͳKj-‹FµÚLÉ›(„²ÙX›hª,ʨÚl¨¥-J)IA‘”š™(,&bi3XÛ™±$X‚¤‹&¥4ÖZŠÆ±šFÑ’K[lµ¬R&m¦$£¨a b(¨¦’V™É*ÆضŦjÚ1e¤©¡–¶RÉmMM²Ôi«FQDEŒÑY%#/f¤ÔmŒÍ›*"„ƒQ’L¤™‘Dd#1¤¶ŠelÇñÿ±õ§ú~jsüø³B Oëý ÌdBD‰V0ë_ýÅšÅ\¬4›=]Wñ<[Ç7ðÿoKze7q˺þO>vóðþ‹ˆhó%qÿŽµÄ”ÀA%‚VÆ,º&©%UPÕTte#ØöG¥êö×ú.Óþß™uþ¯íò¼ÈÛi¯,€Üò%„ŒEõsÕ£þ‹n¶;m"‚‡Ã××N-ÁÖZX%'uÔäšu;brxy‘J1°á‹³\wz(ÓòÎ]ióŒŸì¤kÛ ÕdÔHí/±½:ô×F{+‡““G]“¼êQ™ª™¤Í™@À…°¤ŠµUIŽ¼ûfsW"2l=û\Åowƒ4fS®4¹–%!L@P‘#%Í®Åö1¤gÀ®È¶™|[›#7¦æ’ÁdŒ”^¼ÊŒfH‡:¡SÓ÷éCþ éãÚ0#FŸ'|»ªæºhñ\Kò.h"»ÐJE‚Å}ÿökD;^/LV¢eÙ±SShR,ürV?ÉÉ"˜Ò¼Z°Yâ$£Çöî\P?ìŒ=XQ÷}0 ÐÖXnÙð>Ïõk×£•=Nª+9P®îãF€ÑÖèæî]P ¬sãËÌ”råÑwÊæÝ,†Š*æÝ<î‹êºùKol[îB¼[\Šf¾.EmâæñÒ'ñ·j¼s›…ñùâ aãÆa#/›O,¡¶cÓ+ˆm·Š}~uovô÷U\ÒmÝrŽ¸Ók¼ç“,oRŒF¶Â46€8šë•Þwc”t®\ÉE.»¤‘Îd+ìU¼¼íþýý+Ëà½mªR)BŠ‘•‚ÐûleÓ'–¬#1üè¤OJš~¶¦#/+j~OLHeàÒ]ü]¹_ßO¿«À¨Tž»_Ö_ˆ†A†bI>ʘ¹—óKH(#lg}}1Žhøæ¹k›ÿ pš8¢*–ËahGbódlAîTi¯M¾›’(3Éë8(™ŽÍÙs‰ñÉù5}”|±ÉÃUíâaðøkÕš08 ²Kç^—\–f5ŠÛË[Ãܯ&¶¯ƒâ„-”ƒÊª)OݯÛ~ŽyßC¹;7ktä×ΡLciœ…’2ÆTè—u»3¼”ROÅÆ-¹dÂ6d#g£%C°%⇆K-2Å”ÂØZsiP;¨¥ç•ãî~x©Ñ\¤]VYU@‚Ÿf.àÒA&ñ’HàÊ}á4ùu…‘69×·yçõUö·“ôºf]Ú“œh£ÊŠª‚Ä7Ím‰Z¾)t4=]n‚^î{«Ï]=c f{:_&föÒ±<ÙZøWVcJh `²" UãrɨÞ9j*ßíþÏo&4úIµì®Y0Ÿ´ú¿«ï{uŠwVæÒ„Ñðþ‡›^”l‰ŒbI5%'ÁÍÊïwZ·K§-âvâ‚`MS•ÖR’.•ˆÁJIME¶ó¿õëx@Ò’[IP÷»–-ûrÝ{»o\clP›•k‚ÐSÜ%kUBÑH,ŠAdD/¡øUãƒIoÐ#E_’ìaõäøïš°ÌâȈÀŽ48‰QãPøóêXeˆ®•µ›3ðI0‚#ítü½mÙ„Û&¿2ä—áîÕòwÉѦׅ…á¨$`²¡)!³iè¹9tÂçZú²üNõi«à/™:”y¦7J)”"’ê)ìÞdoåW<*ÊrrËfõ`Å#.T`)À„9hàaZáÆ!ŒR'8^SÉhTþˆ£G†ˆÒÛKM[ͽá…â¢lõ…O£#1×ÉlV–ŠEFRpg«0"<™% kTt©@'…SuðoÙALáE*}øî4•g“á‡^$Þüb«‚m/?øiî®)bÙˆn›8@„»šQRA‡t_¥Ê»aØöˆ!£#/:óÁ‡ƒB‡Q$)#1JLÂŒÊ4¦~WO¾¯³}ØügqÔ5„žUá¢öÙå|è¾½À1ÙAMï\Ãʪ™s)KU#1DD|Z’ãT©µt{n…ó³á~pª9\•”®ž9¿†Jj!(#/X+sé_[Sn5¯ša%ȃ‘Y¦‰g{·-6A„8ܤ3óÅ»¾p™©òÓzúf²&™Ñ Ê=SZÏÞû£eQ´ÎëC‹þÈã˜Ý,8:(œÚ>ÖP§z~¾·×âòu5Á1`èÙ÷ûlË^¯œÔ/žx®KåUS¨<g“çïtŸNtI¢Cµ?F,…«Ï‚@8!<Xj'x¿÷3,Ú!½ÃY>‡un’3T·å]¶EèÊ´Lc1×™£&Uí“ΗÕ:PâyË‹âð‰¯_·‰½ã«—¦FêâFpˆVÜòÖvÞò™`ÜŸb¤blÇdzï†s4çíßz`s¨UZ|¦YI×jµÙPÝ(Ib¨Œ1Gù}—ÿvM(I³Îçb©vŒc8À¬Ø1 )#1†°`¼Î§%±qæ‰ÓÓÀÕÐùdE:´!`ÝËÂmáG6gitü™×lðH`Ò¡ö¥¤nÈcÂ2¸œŠ=}ùLiÈ,†NŠÚΡ?DØGÑßíuR6)ãÏ«~k(úw~4LU«¾íëÛdÑéU6ΞN5ÞA6ÖÙÛ Ö(Æúëš¾ÖÕoqëZ®#/ÿToRi¢Ç¨ÅÞhx×ÁÛº£sui×î:…8gLj±±Ï׺}5Ì==°+~žWÉã²IeT׿¶–“çÚ”Ò£o¨#/_Ï“F÷0Èu4ìí}¦´jU$üQe7EGŸ/Ho¡á¬¯Zóß)®r~RÅ|#1|zˆÞwº¤hõhõêßÚãâB鑆œÜiµ·ßÊÉßu…PÖ´ñH§Ý‘Ôп—ΧÉ2¾ù<Æ8w³™«óSq®ÿn/«!²©¤ü’2¿¯Äöú'k8~ŸOF#.dn"G“H´”’T¿x·Ÿ¨èƒ}îä7+¯]vKq›ðöº•d%~•Ìp×jWËŸÓPnÈÜ‹/—û°¢m¢Â(O#1©¯ÿ]×äþ_𺖟_çãxèÓøÕE~¿ixÄú.ÛôuÞ"ˆ,F¡ùx¦}lðóíÍãåöa\ñºéQåw7kw!IøÑ•¯¹ýÜç¶(²nÆIüím«é›¸IMº“TìºTèªÍD#/pJf{ñs54³ó2Ðí¼å mk4µ“O»-nõ#/3?K?s°¸ÒÊ.©P`/:…ºýivS<Ë_¥ž¯m¶¶â~trÆ[J~iJ¼E|OS’.NýÔ#ë1þ|èýÚ]“ø ·#Ä8G¨àDL4wwÄ9½ºÛ9ô¬ñùÀn˜:°(m)UER`gìÜ}”T^&j¬64Ø›IÖŸÖÒ;¹Œ‹‰OŽá£÷öuO»~ýð=´B(üU^;÷Ń8™/Þô!Ó]7¸{ZÜo×MËk»#1RNîzߣâ^äV5«œä7ಂñÔ|sÁ’âÄ¡AÉ¥Q×›œ±Ô)#/€]$:Æç€Æ›¼£ŠÂvòRG”tÚN⺄CÆñªb‘HaÙ5zÏäc)—Ûüh†9U/&mrSÝç½Ì>‡ÙF7̹–ÀAAïÚ€»Ó]ÜkxÌÄES#1{%¶ÑÛwL<S|ÂR:§~˜¶RŸT¦rJRi]/fÇzãuŠ£êKZϾª$yóÚ#/tõdsŒæ£Ãé§j>Ûáuš(æ”"#1ô¨p:{óg¥Tè…Äj¤¤Z+of/Ÿ;/DågùkG#1€¦ƒlÂ%§ƒ#1\²”‰»:¬ì˜‚ÓÎsÛÏaxÛ(WnN¦I0呦RN/ð¾}þ6FPg<DÕ‹õ£üë{Ïéh\t»&öÿ#.€ÊÙ´eŠò¤ÜŒ†#11Á†&… 뢈S+fv†Æ¥k)lNO"s|®û=+8Ƶˆ¬KJ\95ˆ±%®£±ÐOÿuؽ;®wyÑÌqÌâGag,¼¦2:LçÑeEÄãçN,ã¤Éy¤³ëñ-¶Þšú)¨N^Ó29#/3lD3[ÿÏõhrÁB(ªÆ¡º²,é8^Õs5¯°a0†ÂÁ‹cD°‹Î(44#/!GŒLiþ¨‹`»’É]Üêùd”?k^~e€ˆ«-ꬾ¹®rKyh‡0¨EŠ°šR9~Hi}Ô¾Ô=‰ŸÝ}dÛ¦çOº?ÁL>ë8ùª‰„"ºêÓo7ãFŽpžì}”~ç²e}ËóÃœfHWÍÇ.âM)™ü<¦„¼§Zª-Ç_:ÌËQ°ãw·ÏyÏZÀ)5r„ˆ5)×UHö«ÌKÁÞE;iëgú(Fx¾¾é‰±ÿ<÷½eïë—7Õ·eŽý98…ð†%¡#…üÓ‚{&Ì/"–(å6š"º[QôÉF>¼í;qíô}|9ƒº,Sæí± ÓŽáxpR?DâØûÔ¢"ôÇÇ>üÊOèþ:ëìÿŽYÖx¡ôy¼ø·áˆ,Üõ¯©Ñ8ÿeNÖX•“7Wu-ÉSüaØ«|)tç˜îc}³ºÂl¦XÎ¥¼ØǺµ¡Bʪ8ybäùŽ +¥Vü(<vï9oÌ|fXi¶R_/‡Kñ!œ¢îûèìg£õŒØë9*¨‹1kƒÎ÷y)T:ë0´Ãz¡U/#/WJ¼ü™úŠâ«ÜãêÖ®²–ýõO:Áðí¬óٞæ(fìû“œN£7ºõªz¿s&اZiÛaǶ‰ix?gÆ\Ó³¹i½];¥ž_†õ–—¿¥^¹ }¢Á¾³¬!ÄÆüô÷_Ò‹;«K߆-8lN#/³£ÛTLò¬ª=VïÛ]äˆðIœíqiz 済Ó BYMirÊ?’´[â»sIå¸dGÚUêo»P‚Oi‹;?±¼µxæßkñ«”ب7S·•x¦`‰.»›Mwq® sfF#1öVRöþ¿ÌÒŽÖkF("Ïéé›{©ÙLåa$5 %q¹õ Ê _}aàšEz*A*2ž#/*rCùwÖìÔñÖ‹gÜ…Þ·üqŽ<¸É2ûåðDZj¦vBI~¿=*#1M„ÿ…³ªß$áüÇaÅ6úÅþžU—Ú-ÈtQ¨jQþê(¼øÆF>"?É0Çñþì88ýþz ù¦©m¾›ãµéhæ·ÔÆPEû¡ŽQ¢:U/åSe¿Ã8õ*š¥ph¡œ'çú¯˜Î¦#/ñÓÌƳëÝ1˜2:ÔÔ…Ï ÛiŒmtÒ2Á}[ ã&5Ÿ'=~ˆ+dýl#1Áµ®ypò1(ÃÅï'Òÿ‹´¯ÞÿJ$ÙÍ. ÕùÄ„~Hî™ÎKKLjÊÕÔ‰fÉQ`¨*EI°[ ?ãEý¢Gíí#.|ý]î>µ+öšsQF/F&”µËHêš²ÙÏj9WëíÛnýwèñ8¦¥´_R*dL!@ˆÅŠÆؘÁŠ•u.ì[Š°7]J=Ù©ìJf*ó‚tufj þMvÍÓ³6y°ãLoUQ80±…«|;ò-B&Zj H³â6U’´ODßDX[¿w?~¸\®èã.j8Ú…Ù<‹>Pä#/u2¹ëg|ºÃê¹E$‡K+?ã¬ùµs6¾È´ú›¾^E®Á±DÍ@Ÿ,¾2Ñ´mðöÛí>4;rzØŠî¡(ü‚¬}øáõa™•4˜[ù7~úª<M`¨¨šþ4ÁyÒÁ¨2£Yª‡•;ŒâÑ%ÝCÝuªR#3RP¥Q(X£/LRâ€ÉQT VTÕã·væñ;\Š\u]Á¢ˆ—ÒEV(Â}µìÎcÒß“§S‚ð:»ÝË¿9?mŠÀ½hÝýŒI8•$’ä™d°½`R·ƒonUÿ®P@ÊÇëÓbX÷Ô}cé €ˆvïÍxÚ/¾!Õ¬|Y(‘‚+”( 9TYß×õÿ»äüñÒå)äQ¬øµ¾Yòã‹üµ_ØžöœnØ`O‚O“ £>ô8¼Yvö‡1PG•½Õ×Ê2¥1²lÛà“B#/Ÿe#1¬Á籘5ËxF±‘§&_¡¶¼XµdoTƒAì×óéÎÛ‹o¦&Å 7ºV`åÜÂóÅÀ¶}•ÖÐÙ@–îõã#/²68¯¡‘`¢X(´£â™ˆy)e‹8·ÆçívË}Â#1>˜yb¾KÑØÑPb°y‰xUP>þŒïÏŠ‹jÛèÐêhDQ‚³Ô&L)ô¶œ¡Xظøg¤@y3YgTA¥±mQg•:óaÎÿî6JŽ%µWä2/"Ïb-_w–¬+Ëw½™<ÚY-ìU#1¶b-U<×·ùe¯û}Ó;Øà$\»‹hpåݸ¸€û¾¸fï{ÃóÍ5>-}ìøÊ|ùÇN^˜ É#1]ðirºzXtM¦Õôu4¤Š*ÀÊM´7á(XÎ54oj L c‡#a;BK/á‚hwv§úpé.øˆïÉ${–``7çˆn¶àZgL6ÎûöpÊf¢1øMs4ÑXTFP‰Hý!{ø¥]ü·M²0Ê›×Ù˽Õ{hjªá&wÿníÂ¥`¬Í‰Mü³W—½à†©–§aãéQM"Šw??” Cºžéó:€•›Ãì@øùz~ÀÁ`øóßTâø×Gvƒé™Âá?ÃήyüFÅ/_Kp$=Ì&9½*Ë?“ãiΈýä2ˇ)ÑEÎgÀ‘ðƒsQ´Õ({Ö)q—j6ÓÅ.3´ˆ X0`ÔÆYèL™%•á¸ÆW‰é·Vbµ[ÌÜ”#Cœ§‚¥‚™†$9•>rŠÁH¡„Õ’PmST±~÷™û+©_ž´rîºìÓstf½°ú¯VzÔºýöÇGŽÅíïŒõÜ #Ê%ÐQ&\RdPévÊC¸pͪªñÌÚô_:Œb«'ƒêAäâ¸ÓIèzßÕ[ûuãcéRñQò#1/`"#1" ˜»]ŠÞÝìâì¶8Á RQ@媱¨xWEX8ÿ’§gl3Ï·S»¸û˜'¦iŽl¢?±Kòmàö‰Ù¯þªë¤§Û™Ew¹áB Î@F@QFÍ@—ÿu=yÙS®:$ÿ·7s¥ã(LÎv9ø÷ð×N¸’¯vñ#D>ïé·€6:Úâá»z#/Hv(À1À)5ªjx²:ýsðh ¼z+8,(v¿–ŽšÆTrÒûJ&ùæs6fYκ/ÿ|[/Jó£Œï´¿î ‚zvN½3z»gG(ûDl°så2¼Ÿn*ÓÆZÁ.õèûp„ì[¦”F+Ë|×F~írŽÚ¾¨ÄíBðç©Ñï‡:|¸ð"¶Ž0%«ëçÎ_ŽÆ¾YÎ/ýQÐu%‘V»@7bÐÍ .å^]T…»cžåCW=ÈüHÊ4#}jž{D,ìN/ðu°Õ%A¬bB‚£S9J¹Yâ¿%:"Mƒ@h:TuòÐŽ³Ôêl8U@ä¼XÏeTFîá«nNLüÝü·Y+u&/ÏbUÈòš+';ªgÌ¥t6܆öäe¼‹,#1J"\ÇoÔõÁÓŸˆ:îMêàïbé#1ñ{p]úsær]pè"³¹”8ò”ãœkv~u±ÊIÿ=Ms‹÷¨Øê+úrã£ÒK¼€Ûñ[›èá›À‰›r#X>sèÓôå#ÇÿXNHr0àÚ_Æÿ-ßëy¹§ ÏL‰TE©G°À_9õfìý—EÇ_¤Æß·»$SoŠä“À%—ÒïÀØaÞ#r\/*¯ò…Ž7!÷r0êêëà~_`"nßú–ÈI€€ñùšá" ‘ —ú÷MIþ+–÷²ã˜®Ï5uç‡UÅÉ,Ê–mY øw+h8ÐC‹ŸáÔ;€r_/O8Ù¦Ê$[“;³—«O£ÃMC¹ý™#/a^¯æÈá.nU^ß]˜D|%:¾ÅÕ®¾|NÓQéT²ŽDJ9×öY“îú;ŸßÀò&emAÞ@„$ Œü¿Ã¿—³úxDÆ›½#/öãÿVwÏF+Æp-¦ƒpÞùÈP Ô"²BÖˆ§šeÍŶíãXþÜëÈÚd·èTD…ðþ÷ž½šÿÇÛ© põjæÈE“SÏ¡3Œm!þáâ`¯Æuf>ê§T¤³(Ï8 ÏçèÚ\â]~>ýtËå‹wZ:£M%¶²AÀg©þ‹]"V÷úf/æN°Ì68Q4`Xè D.j›tW6#]M$<¾ZìÿE¾ˆŒbôªâô1†¤Mªñí-ßñÍd¾ ‰lúk¯m=ÙQ€ñ>× .„ìÓêœF‚ýþºÂt¨›å›þŸÿgû¦4šÔEõŒ@^ŒcÂÝóüÒÁs¹æYÄÌÔì3*|¾TË—Å<T™/øÖ…~dõæ‚qŸ¾nÿ¨Ó…ÎÃG;ϯ÷áú^µ‡íË0‚ϲä—5í9CôÖø8.C¡²£Þß6áôü^—ŒÜn®û¯áè¸Ø¦våßbJ܉Hj]ÓýT’>ë'_è}?Ã8£;è2ÕGk²=CGGÇ¿5»|/°?ä¤_†=JÍq·ÕÑ”NC2GÅü}qŒHHø ³Uù„E¢³äÈ=Ñ÷tôÒ|]qM1H(<šï*°Î^»ï]ò³˜#/‚ŒË*uU_Îj• bP¢L…H$‹¸#/Ënõ¬í)¯møÌY|GísîÇð°u&á—Ó-”øBó"!'t$óþÎ2`SÿþØunp…¾×éמ›#§mÍ1"0î–b8&DGòS2ø‰9 ‘þrâ®ÎåˆÀ½wêÛÛ£©\ëûÏã¹Ó=åºèª—\Kn˜-îí´#/N‘W6KŸÖ˜[‘uðÅ<=Åÿ‡ÃÜh`a#/è®Î¾ìEj·Œ°Îš/Pýë¼G#.©F!)qIÑfâNÑ£z¦ãlÂ,Áš…µœ-rg÷EžP€PÕ‘+D«i%#/伪¡#ö|½']ü¢3ÙâxÙŠß¼¤Œ×”ÿ½æyìVòÈͽK±þÂå¡D9ÁNc8pMµZíõåöºLÈ¢4û(¦ÅÚ©XѬ”heC-ªËùtUw¬Á|>ˆ®7íÄÀ‡»€‘~&,ïØ>çKÚuÑŽJ[aµíLjôÓ¯½±ÆÔaœ¿‡•yÛ¡_÷Ko•<ø8n…Í«V–Ë;ðÏyµÀÿL ¨gU=HbPm9`,d’§>—ÒR0yâ&¡žq'½6“bÂ0y*óëöäámåÞ·ƒ¢„©:gðï!A”ù#,¤eøÑÞKrE üÍøôÚ)"æ#1«œhª(ª„ä¶ÿ;&>‹ÎÈ'~9š]qÕ§ê‹UÀGøºž}Mñÿ3AäÜXrJ’Þ›¢~”®f )#fuå{°ûjá\¼³²xȹàÒ軯†¹¿3S§#dû—ÏÛJÜä°¿5Û>IÍYüþûþíæÀ-¡Sé#/w²\KZÅQPC6^}±d²@æ0m˜¤][Wî{µIé:i†&v’mþµýVÿšÆ„NÄÅ;Ç¡¿lSvLöYjÑLì»âXi&»Ì0º§ý?^ÎÏÆCS{STª¦šÛ‹#o*Š….×p†e-ÜÆ;?ÊâÛ˜~¢#/ˆp™Ñ«Ú ’O$DG$”#.Z°TLö>7"ëã¾w¦È©lz¿8ëÆÊ2ë<8¨D&Q¬Tiò÷5ýEt–AÄìÙãK{£´âÊÿ‡¸=oÖe4™õï±Le`ttGd”-Ǻ§˜1d·âú߇È#/<ßQõþ»¡†Ç½„e$€R D[Üž‚òH.Óì…#/]_#1uV8‘MFƒL6Èa ‡÷Ǫ^GèáuÛ÷¾Ü+kìlß70,Oˆ(‡)2CP°"éÓ2cÜþÓ~»TžknœçÜ”h°;½–JÅl8w)Æ– tåÁ#/:ëhôŒ_á÷œbÛ£v« SÜ0¦¢chsB]Êc!Q2–Õã;:ÚLm¬„iTô©à㫲‘`¥+Å´Ù0Àצíþò†¼´óðª†ÐéÍ®ÿ•0WsÊ=ß#/¶ðºØn%yJŒeE` ÄUô/7¯*·‡y?TNý3¿©9Øùt;?óƒdã0-vèd¦° @@;µh`$éü7M<UÙðººFsåþª%µFmœÁºë©`WŽÐ‚½ÃfÙÄŽ° Pb¹„ЪL‚êù«óff¨!¶’EkÓ¨@º#‘·ihQgbæ£á‹ž\4ˆA‚”HÊö‘(Û‹ð¢çP°AûÖÕh¤i¼Ô‹tS5Uá÷ªQˆZÚ¡ÊóÎÁUG@¸3±È#1LK:@ÞîúôKSëL^ÒíäüG(/.$„¹F<vàÜß|ñ¦ˆ*I×Nt§n‹ë/K†ö~)Ñ}eªÇ6·›ö®¾6¨#/3ïÝøï¢Sɦ¨ ¡Ë=ÙCÁ“FWèÂÈG=ŠàE…™å[”x‰éyÓ§~;Wyóž:&ã‡nÁè.܇ëg룡Žmð增'¯[Œà‡F»/*ž\ÖÅï¡ŒðóáJèІ¸99ptbÔd*GN©.ï« ‡F]0í„u49™ºËÜ%0æËB¡(yßO«v)×?G-K·*mü8q¬bºlkÖÛ)gUësÑŽ"”„Ž)XåØt;SA D^á#1ŠèF¶.%3°8`±D³=û¦3Ÿ\t¶éwL^ÜŽ·UžqVY6ú²‹'ŸìtÔmu}=²gmÅ—œ‚f?îæ#/ˆ«¾zĉ‘²¡3{aÂÇXôPýá–€Áè´WÎÕ:ðw¶Âÿ\éÝ·¶ÙJ9>6ëÇÝêYiäú½ØôÒžî“û¨ù¸Ç½†¼f,-Þ.è²ö‰„Õ+ Æ66Óc2DIhÈŽ-<vFc.n‘pЦAAAmv¿-s©æíetò2^uøñ¼û½NµïÓ/¦½“§Å憲¨(E –H6†©ŠáLc ó)LOLbkëãá‚Ŷa¸Š™ÁÇb´€>!܆DÉÜ5R,ú½<÷Ìœb<\2ÀÂ4MØf¨*©Cñ¹ª™ö^֊‹O(Dþ²§,v<WGÇó€ÊÉøW´0u¹VO)r™ n($›ÜÒKPºÐf¹0ì`Xïmö¸Šç="N8ÉÁÃp½¦¨ýêR˱¤[ìçš,0p¨¨S"Ðoœ*ñ“!WP#ìà =ÐCKI¨š#/çgÍfoצ´4(eL$Æ;¤,zæ,M:×OoXHÆ@ëh7ŸbhŸg†;šQ¿ÆMíSêè#1Û2Òùˆ*#. Rãw¾!ù¯9é@±ø[k†3&kϧÂÕœ?hÇZG÷¶rqÕ›JÄòÐ<Êlú®¶DuKZmÿ¿ùõy4¢r=¢ÆPÞ{>è•cº.uYÁ8cÆáh¨nÔÆ—Di#/î]&7]Ì£mn øjhì1ÉaœF’4‘#F¶±zZhµCØÁHWñ¢à&0¶M’aÈŒ™²…ÖXòFTWZ£AƒNè÷?ê·Ø»{?ZvՈ߯r)xb0e#.¦p…2ê ¡(E±¦F(°Mh“(‰#/-‚È°Ëê¤Õ&]Ï=²fèÁdåÔ"±Š06##1ÓB"(È)5ãS|t}“§ÇSŒÎá$oéãáź¯?Y™#17Ú!`—”ï·s+9˜¼é=†Š)X,_C¤À[a2¥Ük{’wŽ‚ï]8+q›f²ôÖB]rÀ#."bȘóŽÄv¦È›ûÆn;4þYÆ‹3YÖôá¶À::ýµˆ0t 6“ìxŒD%eñïϨ~ãÞæ¼ö :ô~†K2ˆÓ/¢#1<ÏòôÔW0ðÒz±ËϺ‹GæÌ®žMëÆãM'Á½è¸v‚eÁqʉhS-¡RÑJc.©cJX†ìƒ’£[ÔÁ¯ŸaésËÓõÔ¹mhœ9 ûC¶K‹3ŒXU ZgäƳsÒñÚŠl*o¿fqÜ•¬ë8,rŸ»ìA°Í„$;~xÜ(¡¥aÊÒ_g%(¬3àgŒï]Ÿ¬šâ1ßÂñ²øàŒQˆöÅ»1ñh5z8x™7ïuÛLàÎ7ƒÂb(KþùAÇñ¶õý3>N11°æ?}¼v¹ÙÛw§~t+ùjý:Ìø™I$ âk£¥´\ÒªÓzžwaS‡Ýƒ£ôv›Óð9З2hI‡ 4 K'‰Ä³â¤»v0zGïÎ˦+oà;~ÔÁe9y:8øMâd>7$…´ö%ˆ#@´ÿGÀgöwg¸@@EV¤*i·Â?PkH,´Š‚ú@ÈJ™eriŠ#.ÒÀí‚“E8zÜ2›ˆâŒéå`Ü{îcO‘zQ:"çø|rX ¸:a&C š#Hz³Ä™dEdlåÒ™©ˆ‹#1°ÂRM"ƒk$¡‘fDIÓÒN¬Ž¸#/Ši¨R(ÇH¢Ž+È¡#BÉtÇ/u°9…b¨dRÑhŒ9e®0"!.$Óçß0‚l6ãm6ÙfÝG2h‹bd+"Zq¶àëØÄ;ŒHÔF=3”’AB j™s/GfxÏmuéMð z* iI¾þøÊŠkùOI+§j!—.tòçÃÓž}9ãËQLi7™pPÒ¸GÙ³)Ùž?”õ€ióMÃuY§ÃÅIßÎϬ3tANÜÞ•«àÇqî§ÏtäcyH'õ9R-Ñì±ÿ–œUPÍ_™í©;ÇòyÍ{¶„øgq¤F7i’Ëd4îsh(±šF²Ñdt¹‹(ªÁå^ìýúçÚYŽ0ƒˆÎ3]ã6ò‡®˜Ã¯Fø}ç9c©Äȯ0Á$íÚå™ñsæ};Åî[À#œÍ.ÆØNeFeB†Ã1ËĬ¢¹`í…fKL«Ú¢¬±•ï‚/HíQšîåáÝSSk‡Ðïe/ŒºTù6ÙQµíîÕvãË÷ùÑçDtÁöø#1Øã³í)É+£ë)ÜïŸ÷ù¶?r4Jtf*¥Ïƒ}Ù]«¿óÇáÛt™1Üy’@Ê`Ÿæ™]¸‹,.%’?Üœ)ǘ~æ÷jAôr.R1¡þbŒ!Ú‚aèçJRä€TGÍKå1gEJªÏ7,fʧUZ©ÞEü÷9D}Õªw:Æë6Ê”É4Ðî1TOË1õ^yßá§äcõøÎÀöìêç‚„„Ã"„âŸK£Kn\™V’#P¬ÕCPÅììªùáU¿êü׎SQŸ;á•MTÂ'\;u½íþW¼ç(xð£›>Ãe:{±F=Ü>g¾`¤×ÛF3€(à ±.PB¡¤™úuÿ#/ò2÷#.ŽèªÆø|m~UÆqÙÅý\±»<½YÐøš5¹fD‘·s¯Ô¯$äØÜÈu=¾Hƒ‘j€uêuÖ¾VÜÓRk:Lb” ²,Dš0ÝÛµÓb5s\“Z!F$ Ð&†)û=b!c¬]¥²V#0[ìiÄZ0l(dµ¬ýÊè×nÛjÂÞ#.㧀Ä-õJ쪾i–Êã2úΨé8Ôçð#1HÃâ|CàÓ¤OÁ(7úüºËþÂÎUÕòè¢õ[õÏÅû¡¿rCçzì„XEEÇ«è›MÑ»ÚùËÖÀ4sG#u.Î2PᨋÚÁ*Žv%,#/Yù°ü6ßü&agë~½]=‰ „—#1‰Aî `G2x¢;†#1¶l°fAWòòsòG/va“¸ä b)êÄ#/Ò@ÿ$÷&öX«55þríuôE§é¡ÄE>y<·óÎØDùÿEÆòÜí¨½‰{ÖØoï¿(K2Lñ–4¾±,NDóH(sJ T‹®1°W®‚¦‘Hžénì“õ<|Kø‚4v®r.Ñ:XöÁrïûÿsuüûÒ¾L8#.äEP-F³'2²1N¸#1{†ùrw¿m,a6n_Dïé綗žÎ;ú@RpT‹qºZ³ú¨‡““Ó†ÉlóQkjÉ#/äÅÃÏíó;Oåãû«í¾ÙðÈ]ôø¡˜DztìÜ&8žf`<™§[$–bâ :2Uæ›Ð¤‰oöon#6‹©˜JZgàs& vaüe•Ù¬áÓôiDÊÕÉõ8.Âã´…UG3ƒ)@Ä«5°±Üµ…»vîwðn`íÖàšE@û·û<8ýC§7ê7’eÜw”àíå±R2¯¤Ç|y´,=íÉ™$òj6¨C)ÐFiÒæ™A‰V?Ý¿ö ‰¶š;¿õ‚&/‡õÚ²úÿwÏ€ÊMÞ£êù9mç©~,ˆU*ªS?W“UøBÍ4ípÎ!ûOÛ(ü:¾Çí××ï3{$;ìvCÓ^ïñoK!#1-ˆÏS¶ÏÔëpŠfÔ–M1›*XÊ/Îã14i±WôW±Ûv£E·œK¶Š#1ÂÕ{)É…ï_éî8ß¿î9ñϧúΡþàšo6ç¬ÞpL¸~Þ8mE0USIN0ë ”#Š…4ÏÆúÜˈ°bå]×ku,ÎrÁh>m¶‹Õ5çú+òþy¯?æØú?„¬_!²bQEï™â“êªm!Cßýwë®GÕMò~UhL“]{>|Ëä·é\´)¯ð•nH5JH“ BÛ*Aûâ¹Æ¢Ü4›çÕËéV¿õ:½”mשWÛzI·Éµ^JÚ½oWuÝtH>Æ46‘þ÷ñhý²cî#/ž”®Cš·f¤ii8DYo÷Ue£ä+P7HR#1L2ª&Ad÷» óË`Ö•ÿ"¸8/#.®dÎßÅÉ| –Ëøµa÷FYОŸÅ«/žœÿ—`ìÆ D3%Ü‹Œå®<¨ä‡û!ù_¦t<ë¿ñ¨}#/~¹m34¬ïm}“dµ_#/ß¿Û\çTCõœh½29©Èõü'a;ôô™ÏôÁ´7$AÞ=`¸*¨æÎ4aÈE&=²¸–¸h`¬§]9a«mðûë›É>´|BP?|¨PÁFáriƪb¤Qx¡AM„É;¹Š¤n#ÖÍ@ö—üÁÃ5ƒ£×L¿ž$jŽ3ßø$º°èðiG§#1#úD?kI8&î”Víýv¢,QVÉüÉI&Rx|³.%AÖÁIÏÒƒãFøáÇÙÉ6Õúü˜üÇ—ä·õ·âïÂ>®Á/›æùÅžžuñqIä}–¯¨·ˆ-çoîõlè'f1[»íßö¦¶zóÒÿغüÿyL?SÑ,ÄløÀ»à+õඹ‚ë-t#ñÙÕ½^nnëÙϨ¦{hÜò»¸R€N$UÙÅÜ«²ñU–Òœ¦RÆw¼©_#/Í/{~Q÷ÇoWM‰j5Ýï_ˆSûè¸ñûbÅ—Ôåîÿþ(ïÂÛ4ÖW?÷éŸÁÏÒM—r9+¥‡&þפi^4†i8ʨ´Å>þߧv Ý@T_ÌƘ1á@{<2<ÈÍâ‹J_âÄ«K’5¿ÀZpp</Í y—Ä‘ŠcåÙ,é› ³ÛÊ?ÏÆøs¹´Ñþî»øÕqnã~2ø¥âϯÑ@h£ÑÞùåp:i¦í[3]ã(ÚB€ª+…ƒ6²ÇtÅÝË–‘ôülYd}C¸zBý*GÀ§àϽðêUCOÔ…àŠzX=ણ”¡†ö÷þÛÖ¾¡ŒTiå+mÄ#1*éuOGöçë5?Y¡Ð7Œk–_~8wôìô©6's06ÀSRØmt.±DÛNÂ#.áú¼ƒ3ÝßK[â×ÏóÜL(O¢A‘ÇÇŒ±Òø‚"T‘xf=VÜÒÊOIRÚÈÚÔpê‡Ïful³«–žŒ50müÑ®yîïÆGŠk¬rø†Àé frG•gL¦°þLÅÂA% ŽÆVw‚Œ4ýÌv3¸Ta%rX‡ÿ’E8_,Þ(?¡SÉ5ã#/&Sƒ‘þÚTôÈ.´4òÿOs¤E&#!ŠÁcÀ¸s?ŠcÅ_¼ª~ª6ºPž„¢bʳ<~™ÑÊjÕg#/iŒPM0p…8²÷êmëñ ¼D‘_#.•9ÏW—k<ŒTsŸ)âg–‰Wög`3˨¾úŠÊÜþ9[}þn¼'/iÄŸvžQŸáéäùþ}ªº!É'‹)#1[ª€Ý%AJˆA ÝB[ê*¨ö’¤ÿztæ{½†3ôíïþþKýÀëúlm¸4òð£h6þ¾}ˆkBJé#/¦¹xµÓnm^•¼¾µõí=Ri„òHTV²n7ûöþ&úé#/„?Ñ\s¼#.(÷"H ¨€yº…áäf,vnìÇ£wé×áóý~Nj{~¿Çà6mè킪‹×ÏðÊfÒ”×ô}–ÛgÙg±ÙOÕkzìuãG¢cAKV¹ïÊ‹›Unö2-‚o÷wr;»Ø#/ûp µù]£¯_ÙÙûtß»â§/ÏÆñ W¦1|Ùù³Í…6Úöþ_Õù§ÚÕ.ùÏÞ·¯›š¿åáóç¥õ;œœûõz°rîäŠú•¥7ù´x·äÃŒ¿NQîÙãû{>¡§Ò[ôä!ÃF¿ÝøöýO<u°]BÔ¸øx½ºßNÙÖZuôz58h¥£Ûö]ÔüûFX#/Ó&ÞÉ%7'»ùËãôH/ÈBžtŽ$l>O³Ñ¤º<[Ý9tj”ø˜‹qÅÜ£ìÔÉËŸåòÿ…ß„ÇR–¨¦ÚÁ—³#N6ëí¬øáÒ¼˜vßê²mñÖø ØBýó9Ýe5ü£ÂÛ<Û£wMï~¿¼µ§ŸF)Ó>¿ð±Œ4U´îxéÕ3Û^m»N˜_sæ5,z_ÎîO$2bò9Η±-nÛ<'}¦ñ[;;ŽNtHµ´óþý_#`#/ÖY²VÙ²&Û/`6óAì1¬uðÊÙLKÏo\l;|»|–õ š¦Xã°[³B.`±s6€¹ÃV03/{ž®iúþ8xÙ£]rw'¯›“Öù8óaåÝ¿Ôví&éc¹Þr%ð{wÆ»Ÿ;õ_‡ŠØïâÙnÙsCÈ¡‡1‹K&ë¸óî˜ßƾë×Átõ¿£S¬(âC¼Ý to…¤xQ\RÃÛʢ®ÿÞç›ø{|úÝÌ|l±®½ÓëöðÖ0vhéW~/þ°š†^o?:ö~&—Æ=®ÌÃ߈Ë]ôœGq‡êÜ+¬g¶C¾ýÒ¿àŒNêåà–õœ–ö‡äú~½•ÛšÞ¿#/‚ë>|óU_ì_0•uázsodú§x`g,»êÐÌØð]Ùþ‘œÙû*+H#/OF6è³ËõMâý#/¯“F2áZK‘½£¬G=?fîÙ\}aî8ejŽŸæ¢„zŠiÀÍ:ô/Ùó7Û·Ÿ?ÏþÕÛœ{³;‰Æ¼©ú½Ÿ×æ×*i»õìÜ]«>1ÃþhþÖÞêû|W÷Ã_Ê’ÿ¸ÇgÈù#/XA—³˜r|ݦܡAæq´w|®ùô"Ä r•ÚvæïúmýnÌ6ée÷ìþN¸}[yõt[¶å£ÒÎvˆýQ9Õþ[þvùŽ2Ž#.ËwÌßåÙÿ»èhjý¹gN°ñ…49èäÃ^ñÖGƒÅ×ôù'n¼~OwøÙuÞ»m<üÜÚÞŸYûÎ~¯°ýå¿ÕߧwqåŠgî~'…kgåi*4úL?ÔRóý¼ró6f—g™¿¯îÓóº¿Ë³ó%¹Pª*#.Àçp#1å¡hX(Œ˜«³Óåôùc /úOÕç’Ó³8ììEÊëèªÑþ,®¾Øj#1ÏêS”\œus|øÇå 1û.QpH#1./E€wÏQ›³Sùßø¯ m*Ås$ü>ßw°'çà:môvŠ};~ï’þ|y´Ÿ«ß–Œ;rñx~Bæf×áÖ;pËÑ=n~³»ð#¶ü¶…²½õé]{>MXsyMH]z9?úàïAÞPií#¯Ã»ÓŸœnð÷Ó¿“·óàÿ!«õûΣâxéüoÊYñCÆ? £Ðñä™\~ië¦KÊ<(þ›KÿÈf“öfÓ`Ã÷*#ýìÃ?S-ét½ƒpú±Nü{5mî{ÿ~ºmû~JêpÚœ{ùZ·}ýv#/¢Á»áN{£Ãî˪{¢™£·]<Ó¬z1õxrjOúØlQÌ,Ô?I”HjP'ã×ëêçäçâê>ĵ<CçróAÊñÈ0ú5<‘xà§ó ØÏÛÈÔø!P„Õñí¾b¹ä¼›ôÞŽª%¡"‰À7ø§/6nwWã;¾ï^%ån¢Þ¼üõ2=?¥‡¦TùûÛ‘Ùæÿ•tó;øO…fL—á>¶žY¢”äþŸž°Ð87$)çUèÃÉ1}ßN°n¾œ:¾NT–¨oÖÁ&ºXØ)힉ƒžÕdCåBr°I†}àP¢ËtóÉù:²Ër–’é;ROÇ`È9üÖïô&-'øœ$œŽ‚»%c:è)ƒèšŸ'Yc+ÇÃOuÅîË*W¢þn¼pB†;ˆŽ¦`4¾»#€ƒ×äQ.¾?w%ЪeÉGfõÞìH¸{&gI`C¨ —æÜAÐ7ŠkBBnýÜuY瞟žß`N#=ðöoý£‡¶°âÂh;¶¿Â#.·]<xÛ"½€ËÆéÍÖplQ\9¹N3çËŒõ®#/Ñ·ÝZû+O¹³¹Fƶ0ø¸©<Ÿi›yÜ·r×+0íkb¥æ¥Ldö¼ÝãÇ—eÅ_ZëÚ`ëaÐ'Ÿ@Ã!¿‰ü‡TÌ^DvC¦èÔÙ-–ñ¬Kª:7×êÚ( húóçÈ#1é6âOÝëxÓ–Ô•mÑ¥†á³”±¦ímZg,î´ÊÚò»óÞôŠ¸hóc^/~!G ë̾wøïv8>ïxzgA );H¡‘שñïÇYèØ!ÒȺA¤Ä-ofó^ÿ¦M°Ô´øÙèù2ãAšxç¾#.‚«¿Ûûí·èáZû‡-ÑÝîÀXsϬø¤#Âõ‘]íâ²ú˜X1È1#.NÄ7#G¤2}q£¨é³ÔtÝMDZaoRN5; s"ŽÝ:@ú¬ÑÑžÎó-;˜îÊ÷ã÷~Qï†/_Ÿï#/:ËG‰Ú'¾£^Î¥KrDÍ™ù‹á*3c¼õkD„žü~/§8ú¾&LÞ/PÚ♟žÜF/=Ài?¿×&ÖÃÎyåϱv8Ñå‡c_•GÍ™4]¢ý¾‹üñ‰é¥LRI˱ôÁŠMòmŒ¤öF/ulu³ÜYLpåü#õõc„Ýúe#/>zú†pï'ˆ€6”*½2`cÎÄ’-sŒûOÝ꘨þ¿É’©Nlã"<Nôq¿úˆ½’rÍYy>HØüQÌ#.?4µàÔËã¨Ê7»ö|~nßíþ¤Yýd>²Åû¢ÅcÿS!þ‚ÔfeÆžFÑ[r7+,$M¨ÁFA„aIåí€V6j”A¶ÅHâæ#1+EJ$Ó¶ #D49ŒŒcIàâUV”nmM¶‡önÔò$1¶¸}=3†´Ö¤Olƒ&D#u„c±GMQ5†5i’SjÅCCt!Ij²4±[a™F6ÛŽF#@âŠìôɦW† „Y‡ÉIR#·êÒ»ÕuvÜ9.„¹ÙFÝE¡+÷Ä["É´ë+?«øo…2"¹ÃŒäˆD#1WóßýÝèz(dÉvẰ\`±³øÿIhf ä·²a—û5®{¹¼ÁÊ E5z*,mM@¼.KÞ¾®}ÛËúoÓ/‡ë¿²|¹9…N©ßØPefeÝ‚%PU67.&q‰Ë$Ì\Ð"JL#/ÉbRTc˜BÕ¨,M±ÄÈÙ…¤’© ÅUÌ •t™·×à:þÍižq»æÏòøó\³µEz=é£ÁOÙlºŽ27õêÍKáÖDW;ÅÃU;#/¶ê–h Îw+VºÆn]¶y}ÏÁþK5ù:}ö5üþ×îÝùøzø zʽ~Lt/Ûöz#Þ§/ϳ³«o°4”O$ñ~_ÓÛ•ŸF–iÓÓ íœDuf6naŸõOJʃû%Wú^"êU`×Âö×u¸|µÙã#1ßõççÌî¾¾1Qx¿yØHð€pÎSÈá¯/gH$‰ ìÁÞ2íV¬z{þ/Ëð_¯“áÓ«ÛàQ-B<ÅxŽŽ.3šª›|Ûܶ>¿bïìeòÊî<:;óã2óG_±ys ÂÂw~w»ñr8¹Îg¹]kša†!úûï³óÿ]™è6©—éÙãû¶4ütÙäm¿Ò휺çÊM¸¢nºßß=Ú;¦-’È …ýñ}¡oîóa×çVF‘˜0Wݨ\p?ª)âpÃû}ÜÝÁ‰£U¯3¹¸‚˜bÅJ’+q‰ÿuš¤xÆ)Nù/³¶ 15¥#/ÒÄÖ¬3GžysmÜÞy“Ç5™J“dScêŠÛI˜ºÖxjMCeUkÝHÙŠ<µŠ#h 2(ì+²¡‘Æ•re+-ŸÙª-"8?%%mëQÖ…_”&6ÉËM¨1ƒ°µB²’`£$éÞN3‹™…#ñÈJ”¿·#/#1ãB742#/.DH+F"•‹Ë/SK¶ÉtØ7ƒQ&Ñ%ZaQ1N-N³µQiˆ#M\eqŠL©msZ$…¡Ã|ñImh £MFÁšZ†<ê6ˆøÉÓ{5‘F…8Ô”E‘,3_ìÁ`·nœÊ‹ÎáÒ„AêÐGEšT#1ñ¨œ”uV‚(3Ê¢:#/ÖŠ¼S*1¸#/•ÖJF! à ¥ÕšÎ ä|ªÃAI*Í[0‰VXWk(t¬‚R;:¥áCbˆ3Ë[³põó}¼8º‚µÚiöŒù[eŸïnNx\%~8—ŠÂ#/[…σyBÖ°té²_sCãö'qañm—̨¤ôj±tvåóý¶'²?Îì¬Ðnôßæ¦pH–|%SÈyÿÅCòÃJ-®>kÕÙYñf¾‘A0«““¤bGaëÔɼç(¥H®N‡vMÜñBx{<es²¬zÂ÷\¿!~™qÑd$)^_õO]8°Íµ=k²ÓNž½˜‡æùú¾ ‰‘³‡ökjÉØn”ƒµ:ÖØõþ+šÛt*-áÐëUÄ/q¼€»ˆé$#+ùÊvA#.c´ôAó8;bû¯ƒÙHèßÍA1£ÍóÏ y†"çºaE©ÅËíô2ïPzÙ‰$¹ÊªàÎ\ø0êŠSÈ(P@d䳜‚phô¤Fôô3ÓMB‰‡yIÉÏý§!Òúý0m?3Þ$€Õú6莕rÎw]¶rÁµGn1gŽÙ³·#.M¤’P˜páÛýØ:A,ïû“—8Ο¹mA/?/cß¾íDî42`×¢ä#_Ãl‡ÚÜ‘®Î…Á¥Uí=rGKø°GŒ.“-1UÁÏ$˜4†‰‡áÕ·gXÒ/÷àÃO-rê©‚8vÓ‡0Þ"´ëë¥þÿ颼ՔË@þ"Äu#1©‚Š²¿ªŠ]#/6Õ#c$#ˆÉb‰;° ý9tÞm¢^ŠFA»²È•SÇÝû¹ÿ_·ô3ãòYñuŽŸ¿ÆÒ#1·â1ŸooÉ+Æè©B5¬Ø H,X"SS×lX{$EsÊêÁuêâh&7pÄ`É24U.Ä`™ª3уh¦ebàd55P£d\b£ðËA„BzÇŽjæ<ù¨«0d(Ôl+É5ƒÁÁ˜e”FLØŒ€Uô=\Ú^%ïÀˆÄÓ#1÷mºo˜ò8´èë§o³#1ш;ü¹A³`ØÙ7cUPßÙ†hjÁFâ~S,ÄT®íÎ#/#1¶-¡;<fG*‘ÍZ¨®dÛÈvÐk.«¬ƒ"Ž(ÊV8Y"f÷Q,eP)*(˜-†Ó‘2ņ a¶P1íŃFÅ£Áv#/viN¡»"‹„ÆÚN¶‡]²Á°d;Ót0KÊ-™vÂ5LQÇiýïóhÑ ñvøÃ:ÃxæמÝtôïRáuÝï<òõä¶%TlIíxYVÞjaå¹[;Âh\c3Ç<G‘„…¥N*+&ýºüOÇ‘ëóJP¡A·¢íœx¾;!Píô(VÒ^<ùõ«ŽIŸ¿F‡%RÓ)’nj#1|ݯÝgå2œL6Ãûzƒ„ËŸçûÿ/HüÃ=ÿYú¸×ÝŽ‰ºIèäÞ2/ózÁ>“ñ‰f×£›OŸÒ(OH=€§éÉ’vË Òǃ4FYd¶bÅ…¥UO×Jª `Ó#ò]áv,üx´`q#Ñ7]8iF4ˆTéøLTQTHò,Ž¤G#"D•0†ó~»éê*#/öÍDÌ}º†={9ÌïcÂÃÛ¶i¬KËÊâi¾‚‡Uv<„WFÞ`i¤´JÛÒшŒx Ô`ê£Û£cxµ`:¼¬²6ÛnÄ vã>OÏöYß:ˆP'ÖÕ)›·9TL¿Vº(âͧwöÉçòÊý~Ÿo7ñã×ÓáçÍ™Þî"üµw“ãæ}ßíús×æÖÄ,5ë}”ÛÜ]«öÐØ—=–ÁVEËJÐdn-ЉŽŽÒÞg³ÆiéR_rÅE%ÓÝz›§]Ä`ŸDŒuv¦`ÈÐûÈën6ûUFRw…u—£a‰¸á#qGƒ‡ê0+v<£C¨¤j#/´â+£qÈÇ/G$F7Ž%dFãY×33¼\Qlà†fæRªl‘A‘XP„E“FƒL3€L1Tî¾$”,B7ÕG0ÅeˆÝ9È0;-äèÈb×,‹oÎ0TccæCB;#/ʺs|6l]é7w.²I‘QÒCë”ÛÙ¡Vi#.Ôœna‘6bés“*:HT'‹Ý ÒÔ4ã#1Òcìyb5¨ÍIü0…my¹‘IÅÆàVÆíŠÀÔ `éC,‘e²ÉÂ2™ BMZþâkYþ+Ë¿¥+ðIS<!pÈDÈfó0ûñÐ]ú¹"(TÕr{^s™¬3 ù³ÂßLnÌ>ež6W´û…?gôy1¤Ú;¸½»‚]ïóëï²OÑæš ƒpzT“˜ûŸ&›hz£^ø|\&d³ªwÒÞ:üö©«w_¥ÙtÙkIJm7Ù¾±O²ô#/+IÈ%<’*Å#.èTÙLP,òÌÙ¤_c¸5ÅÌÓU×®ÿ•›mížp"9KŒ—[á™rnHmuKqŒz±µZÆ[H·0d"m½h®IÜçܵሇÙæ(×Åy4o/}-u©ë†«é0Y3½b2%æAì5‹°J‹FlÞbX §×XóE^Ú³7¶œ3R¥WÆa! ý à[àhÆ›V ~„“{Ž‹Œ ‘•Àtx@ÃèQülfœÉµ¡¤ºà‘µÚ#Á¸pûuæh‚<²ø„E§+9‡÷?y$†/j ‘}\hV8Æ%d¾·öE*Éà/§KÛÖœÉ,êÓíFIîŒÞµƒÆã8o™–.ÞàýPÖa;Zì&X4¶jmG”í†~Tq°ƒL®®æý°G#/Á ´ÌÚLÇ‹nø¢…öõÎ×Ú1Ö5ôK^Añ‚F$Ø]õá©u#/–æü>4æýn²;Ú¨I$²ÍI$wÜç,ôìÎd|¶|]‚žvË.#/ü¬ëÍŸ©†ÞoÄ;›@G#-Ô Tm)E¼[ç º¶µÚ¹Q”—VŒÈkq¥¸".óâd±1ÕœÐrRÍÇŽ›Nô:î(Ûj;º' ¬›ÌK%c¸o§8Ø(æòÙÝ5o†¥Ó#1°Ÿ±a°¶µ½Üì:#àºEjÇhDèÅ7]]qœÏ&vl©Ÿ1Ʀ¾ƒ`@…-šÝŽ5nuÇWfp–m>(mÃÌß96£7ÙÉ3ÎB(K¦f´WgßÃʤêɹ˛îdäO$ÚÔša´ó6f *Ñ`Õ1 sf¸#à#/n…(”±‡Cæ1n&@Ýäî€÷ßp”¥‘åÛ?³ÒJôv)2€í:ꨩx‡…Pâ“ÞcÆÇ»å¶[S–6}Q½äËÝ‘ÆŒb^L—q¿¥Ò¦«lô4•ÿ¯q‘'‘Š&#/ö‰\àÙ?Se7Ž¸âsq°ª§ß—è ÄMc–ã)äEÌþÔðø¹¥Ë¨‹XÄANMn£e7:3FL@Ðð)Œ†TÜîøïZÊÊY7š{’&‹;¸õ´Ji£ŒÂœÊ£å½Ì»æž)ï›jrŠÝP‘»Ú7Ô&HL“nüÝYþïKnÒW¾|êN“CM6}ï‚Ã~ŸÚ{—kíøÂXä‡Üë#1øòzí>£¢#/51c‹½Àfòø†1ÔÇeÚtuZ[NÁ ðž`5Bi¹g%l¹Î2Æ%$õ¶Mb3¼¢!ôûÌM*KNó:Á·‘yÁËm-ý÷9žš""+š³dÉ+¢û²Å\$Z²(ã¬ê/xdîØX§¥…âÝ*BMê¦6‚ˆuc圵tZôMºW½³mW¾UÛáÉÃcœ#/È×/ί®]ánÃÊ|µÎ¢;Uiõ=²nFQL¨ÅÆ”[-št˜,DÍùèLg³–a©¾Ç4›¦Í6%!l)Š¥\cÚfŽ.£‚º´Ó)F¾êß'w\y&êÄmù¯.‚XfùIs‰=¦xnýåÙ’b!H˜w¼¬p+Zl‹K·]p·ÂháÕÏ^ÛúÄÐñ€[>¬#.½¨(ê6Ò÷dü[àC-|#1½›y©©äüÐýý¿7Õxz¾®Z®³¡„‡Èøçtì¼*8zÑ°ã¦áå0”N²…$¥cµÏ™.ãQ…ú>%í=F‘®ç#ÒY80]Iáåú¥÷{#.„ÜáFÚ~ÓSwSÙÌ#I]Å¿rU†áåè2¸1²ÃSqŒ7ƒ|¸ IΚ)>ŸµD€¿”ryþ¼eeÓ`DFz‡ì/‚}M |¾®Óý\)zŽìœuø>ªáê\›#ã>x†|nÕs Ó¿Ï|<‡Ûe“Iöo™Eˆ×ñå˜H??Ã-™·ýO;¥NGy™g“pañ‰x,ž³(i›M"ÏC1‹mi7ŽéçåðaJ¬°îÍü—ÀW»Å½Ö؃8!EÚK1#1ƒðìä–@ð<TÃÏxËü4í® m؆G(ÖŒ-Ï[žcÛž õßã’Ö?_鿼W¾xç5øó®ŒúŠ`ŠJØ·Á‡¨9×Ú³^9y¹Û¦âj¦HƒÓƒý£³ø¯É7XFX¦ôû“õ%úÙƒÈR~ß÷Ã{Ù7¥z”V8KØîÇÞ^×c§Ôêçõ¶`u·t‘ï'å–œJÎÓ“55‡°:F.„ BÉp1–èÈ°Ÿ±ÉN¡Î]¿ëâvNZ@ßë'€wc«õ<%‰:jñw!{šESå1ºùåpÞYàlŠWÂâ†KIÁÍ9‰#1Q¼Á*Ãú‰nÈÁÆ1†¤v(™}©/—úÐk8`|˜aݲœxÇ;÷ݵʹØãsª4¬æ´¨#1–£‚ªÃbi²B.z˜—ÄKÁœbæ#.} ¶8ÎØKQdBLIªÙNæµEªÇU竇v¼õ’…KÒ ‹de²%ýðT'Õ±À§ÇZÔ¾#1çÈìáÁc7ûÿö÷ø©÷7èÏ·êó¢;¬hÓžíVÅÓæ#/3êÈTî¿x½‘×M¦tGYáRƦŸ(Ê,HÙz¡lßíÁQñ~ø}âÛ¹£sà Ýè][Þ·wvêΪ®Ç0°ÆMÞ_³´ú¼ëùqË0ŽqZâ0äc$ƒœÀx¼¦K»µ·ž@ÂÓ²äꚉ9Èùt~¼šõ¦JIëªõAfÅÙÅ…·dÄuv 0É£}zÓyZëÒváûà᩺u¡»‘ÓU“Ö\?™Zº{¥Õ‹/ö‹¬ó5ÉÆ6«ú™½²<Šs4òìÌÔ”£î¨žœ¿¹7£béCgøýº++ÒZz#/'ém®O{vGýº3fòëòkRé&[iØPúÍý÷sæÆk ìèød!ˆd6î×Ô¦™8‚¼Ž‡O>s¶·7ñ=ü§?ú¾ˆÓ䂸÷vhLº’CàŽ£§Aå‚zW*A?Áršâå»N±9Ë&ŸžÚZLHí¶šQ„Ç¢‚Œb‰ êÂõÖú`xf¤-0è´%ûµ%h<Ž#Žœürß#/²ÝzltXØ·,–(e\תØÏûh…3–ÃOÕ¥ÃáX^˜ŒÈ[Bìrh®{ˆPÃ0Ó0öØ,’F4o'™Ì-Ž†ä"Cð¹ÆU3ïð£÷öñð$\ôÖ}ðÅôh é-D·nüég#Ý=;øS‘ì6ÈÛBæé-¢•J)éÖÛáÊôØ—Ìs)Cå$ÿk»t‚ݧºêÐæ#/öeݦAä¨àëñ¢…«ø!½ó·Àã©ç^[{£¼#1[Ž_IŒêR€sEÍKvµ%RÝ;ÅMñ+Àã’ @•ÌÐ…?ã“0œ}[ˆìäåúž®ÞÄŽÙH¸x‹ºh™æÌ;AÏã ?Ä,P¸]rª”÷¬ BÛÎ+õÅà²W’›™úç3¼)TC$ýžM[˜]°û™?¾×@!½ÿäÜw6ìî«ËÖg^•¯>½öÐ$q[4up¥ÔMíÖ„[´÷¨& j鑦¿…t‹Îw§ùú>º3Ö0ÆÕ±Ò¬¼ßÉpú^¸§8ÙF½‘…ó0¶dÆÞž#/°|XŽ«C£”œ½V¨RP"Hfj¿MŸ±5cã£ÛÝè/ò_8)ã9Ô!>be”<c¥îÅßY×ZkXB”êÓµ)òÂ*tž+ÄêÉ}¶b^js·<Å…ÙõMFÖðÉ7Ûy{ØímñLë Ÿ/ÐÙò¶€»YÌÔïpYÕp°¦ˆèºeò¸è>–·D?«’‚©Ùeòº+P¢¿N>}æyõúm[ßs×Y¹9wΡ4ˆ®·®°W\Ú]Å+HõõÚÉ>}ýÝ»#.à@ÆD¬Ñ!ÉɨŒ”+adzױƶç4„#/-\[2kÕVT·1¥T^a'ŒÓyײĤõ·hb¶Ò‡òˆiÎ}¦¾¶"m¶pÝtZ¹w¨w™‡nýŒŽéd÷&_DlzkA6â}Çß¿=äòdC¸L¿çåúϧœzU®Dç|ù)¤8Õáà7Õ¿Gëp2ñeìî#$ƒŽ4ØX¤Ñ©<ž<ç\–zLd،ɶÝ÷æKtÏ‹Ú8¸‰m¶Ï¡×¾2&áö4öíkª÷»/&ûDQ«Çƒ™è¸›Ñ§q}#/̘m¶Iö0ŠRûÖOÞúNÝÆÉͱ)! ºÒ˜ß‘ݘkz¢òâq„VPKiæBÅ•/´]‚ç¬?ÒSeÎŽ?Ûíç¿O*®ý¨ˆîå:K}½ïB®w(Íä»eUÁ"ËÖÎfcÎÚñV—Ëx'>ýsŒÛp$oGÆ€]ªZ‹{f‰(žÓôÅ@ìR5û¦v66Ìg_¿8µGå>%à}~;QÕÈ$0ÝC“CVárPÒ0΢ïøp9erþØáˆw¶j¨Á³}±æ«²‘͈§ó3tN>{HÛØ¡Ì#:–E¦çvÓ6ͧn'jVËÞ±èˆ%ÝØ‹´”Ùâ{‘fž&14p;9Ý4øo_°’KÑâûÎ|±Ü…Õ£©ÊÚ\´oõ]«·D»îï´<§ïÂl)BÕß¡ÙCùyúNØßi‘4²ÄL=ö²%ÈÛÏ4Ôl2>3Mâ«Ç·ÇUÆÍ‘öØšSÓ¥Kè–dl³Áµ¾6‡^XhRÞ¶:íúaÇÁ6GG!ÿjˆ'qÛ°X¢#/(ñDÔL|Hy€~ª‡)ÞšB-/Asrø+„CàˆÄ/1;DXÎÎÛ4ÎA`^cA†[3Š1Ò£¡¹œªÍ|…žÏju#.¨{hpˆfZ9xÞâ<A‚ò½^ÄbO¾'ç×:;Ǭ€~ÜwÐÆÌÏi¦Ò»ÍÅžtí²âÒçñIâ³ì0æ[#1][\çü% ¤üUYŸ$ é!/y#1A@¡]<xÿ ´_©gIÀ£˜X玿ÈkÅï!%a®«:{çKË•nrÚQA¾ªÖ>½F”ˆåtÞZY›;BY®5ÂX#/öK¢ÖºÎ5àÀPY(îºÍ˃>ÞIè¾Ä¹¬.ÌÀW•¿p.x´Im³…b‰â7«óˆ´]“–ÄÂÁåÖ!#/%f-Y:æÓÉsEWÄéEÔôtElƒSW#1øž°Ò¹ç±”Eÿ?u‰ëµØKÑÍd9t蔲ʹqÂéXüi‰àüÔgë8«î®‰•º<¿²¯Ã(X¶ˆß|qöøÉñY¾þSˆ¥Åñª»éÑú)£JòN)+ƒ¬±¬>6ròNûeñW@x¨ØoµÔs[Ë6ÔjÇ)äau×–#/š¯™'"‡&°ó÷áú>zg›Û¥óYnŒ¹ÂcÁ¯mm|Æ8cb¤_i.|4ëWçìEÈD)yW¹{V~ürK·WÖ¾=Çj¦ÁÛƒì¾ÞZÀo+ƒƒÓ›WÎÃŽöMt¤idÅ—A\”Okéât\(#1²:¨ÝLjõ®¾‡—Zãú³šÄ]zH•¿‹t¦’j¨¤8Š\ñ%{_úñökñòÉ÷3ŒÌü{EÊÉ}ë<_TùØŒ!#.P¼nU_•’|ŠéV:ºiï¹lQ<×?ú^ÍyYÊE¸H=Ífž~°ðù*[G5šï¾Û1tøë´çËm—nu#ÅÕž¾|pžcÅfŒÛÅý¹˜;¥ôãâ×k0Ã5áCME… Ce~ù„ÄŒŒ•ö`ª×ž6ⵑý$šÀŸNþªc¿x‰^œŸí ®•ÑJž5F¬zË܆õÕžŸ0ÐÒ'ƒžèRáÏ/TÂÕ¥µã)O\ݼŒ«²;¶ÅÑÈNz*LE"·âEàè‹,gží6O+D#1_s!?šÁ“Ne#/Žêo9àŽ“2A§qna>/r#¢=Øn™}2yÞé'zˆ·,Ƹ™0üe³Á³`°÷]ãõõÄÎmTb®¨ÕvÊ-¯‹›1SGÒÇ<=pv>,Ñ7]JŠWè8w3¢âlÆ.V2yËÑÐpÏ1Îs…±«*Cqû,®ó¼,ü•D3º¾xãAŒž½_õhé±’B3\‹Ž6Žw‹ûåk´›ÉiÝ.çÀwdCD×h“iáhev°¿¢•ˆq4”]WÅ/ÕXìºSp•icÜ D@ïç|NÑK›¦½ù;×HápÜLÝÑòsOàÂØöÙ„5ôZüaâV©à£÷q³¾yÊG™s°vJ³3]Öæ/j´…:ÆÈè¥^’ÎBæp[%d]#1Ç™ÀHû§—Xc„öûʃ‰¤ËÉóéž17eZy‰ðÿ•ñ£%ÄSe–2#/E0V0¾_›KÜófkñ¦Ù=ÕlN{ÖYü&âég¥…¯Â¯Ù —Š„,©ê¡É‚EùíœzŸr3”JÌí^~’`ºª—RöœvŽM½ñþpX2-x‹§¢?‡XÔx›Úp£¶á(£DEZ™bövh—ÜÁhè¹icõ˜¾ªDÈ?•Cs/’9Žì¥'¿¡ÐÅF\“è•x5`Ncç @§.L5jŠTRi%Žë×fnhÎÅ^hŠú&—$·ÈŠ ä‘Åø/-ó·¼fm'ÕÈi¹úü±¯·9ÚüòbœÄ Q-Û$`Û³(‚å*Åã{bhöeeo²Áˆ«lŽI;ø#1¾Žœã[¹´oÞ',ÂÁ€aZÒ±âÎswîð/Bîþçß{æ"àÏÔÙÌã£&Á•ô·7ÐZîª]:kp¾K%+Y9‡lêëˆÍ#10¾kCÂÕ¶UQ®*ïÆn‹–íí9RNa bg54Ž ¼‰õL-¶êXÁÕß×£@HâÍP¬šÊáµ®§.ÛŸ¦gŒQa‰†«YÄ©y5UÑíu’ã)îh‰wòÆÈÍÛ1X–~xô¦åÝ4¬®5 #.¨Êúk#/ДÁ‰É#15'n·áY¯×¥è}w cÈ]B–˜¡Q(G!×ïhÍx6þß`%h[LV´,#u2Þø>åÞnã#/Ñ—Eïۅϲ™Ë>{•Þ¢¡œ¤AÛÚwAÌìl¨Ûe5ÏLUùgsëd*ι ¦uµü§,óÒZS«.t·àñrÞðæp§CVª%Y‰Ò1Œ¢íy¸Àßdí£Ô 2v‘Ùµ#.²ˆ÷0A¥ëó©#/Þ®`òP"J“G_–>vYÃc=ñ¾+סë‘ÅÎøGëTbrįúíÉöŽRÁËóãØ)ý¯lùöœ‰Ûß3âpoðøNN¼±ÏØ¢Ç<Ô-ß—ÙeFË J¸½v1ÛïËî6¼Vf»óD~+6s«c_óÖ;èáeKiÝAOž[ÍÌfßd›gØ‚?ÒöùÿNoK îîååÜ‚oã]p”Œ<ÎlîX=e~h$ƒÓc(°ó^Væ[NÚZ±ÓÏ$"Ä…ñ¿2k¡çòÚã‹ØûÒ(B5úÿÛ©&ÂHüEm?k™eSr5åÛ³æºæÚUö‡:§3ú±‚q¿ryº~NKøéð³¼ÌZxäWºohëØdŽ#/uËQOG˜`úYk°¥«”<+-Š},ÈŽ9€“ôÿMÜ™â÷,ò‹Š€õG¹©~µsž u~p[ݬɈ·zLJýîä2ò`æ#/òâpËK løª™&eÀëòèq‹¾r/÷ðâ’™éH‡(eÑkp¥pp¦Ö¶ðçºQ²|FRš‹+ºÛªªlŒ´T¨n¥[TJ`MsHd_„–¬¸gP:b¸mC¤BiBûþݬûöÃgÚ‹#/ÿ›áW#U/Ö-/a4¥Vðî»(°õ-áMœùÄyú<ïX:OÇ8“¢0~‹÷Eñœ³0n¶LA®1“w[¡K0¬V*„߀¾*”òØÎŒTÎþ˜5ïÍÈó$.d—Xônîyîro"úÁïS»¹|ã¤ùâ¥,.dußu÷ÊUã÷ëHmÙ¦y˜´gmw›¹8¹³©jJ#fع¾”Ò,ÆZ^òø6ó˜‘öR÷ÎXKKRªa%Û¼wØýXêe3*ä¾#1þAd3<?O[RqGóÈDDÚC˜àÓd·´J#.ÿaÖ\3á…ë¦çe‚¾{yaa3lÎÅqܱ·Dyáqå"‹zV/pôR9±@¤ á6¶g0!°Xrj¤³.ËXÁHS ®çZ‚O/ð#Qó¾a^MlZž-v’[CD”ïµS#Ìûë~¨g ;I¹ ™C>]]Šr»=»iPÆœU=Ìê—'¹Ü ¥JCž5sÂQw…ôÐ4ÂÚò³n$X Z÷µ3Á¢ðº >ÏȾÿbŽñì{gÏ1´#.ÊÄ¥ƒvç÷È_UTï_0,S ©¢.±vÝÙÆ'íÉÃêóßÞ¬´áë÷Éé.UÆQ³cÀ©®aÖ=ðšêç–¯Q¬Æ[ªUŸÙæ%Ø`étÿ/v®‚ü×ÖFVœ¿iô8bµÔÇ&Õ–{íz8…èVÌvj¢%ŽÛÍ{#.á¸ã%¢®¸1éW ã^ng$œ7Ì`Î\¯W3àï˜ô0²˜ã+„šv-‘ž›[kIÅÓ%4?¨ê'IÙ3OMΙ£‡?Cî´Z!'bMØÙ¡”n!M5¨S_N¹UåÓ†ÆûIÞ<¹Î©¯¦ÅéåxÌòª#1z\Ê0€“ýÅßnS[F6ç-4ä?Gcï†wŒv p•éÍï3^%_“³Dëº|˜cíÑd¤¯‚$n.„-rV#1`©Ê¤@ÙïV“Ú¶#|Ü÷¼æfóaðFB¾ï.nðXJøUC#]c$^æëÒUNÓý•öAtŽ8󈦩¥xWPx£Çæ½Í,“ÖÍ“”Jt9n~‰DJµt].X+«ìXÖÊ`Ô¡~õf6[–Ê{óbê¢Z°UêU ßsFyhçÈü1s.3•<sæ¾³pR9‹Ç°îŸGwLI+Ì2‹.‚)3Ôk("UUHSÍ°Ggæ5_3)¢iU‹÷6`Ïm^§‘Óˆò[$™B„f‹1ç—Y}ý>9wûÆ~ƒßp¢VÖ+9,=ì9Û—¶\O}–ÔP.eAú2>5‘-!#.TZyÐѶ%ö É‹õf¬!ÖaO?[µ—b#1,UBKݽ¨ÆeÔåÌëü¢*Ç?&§\hèxíkK»ªÂ˺ãÖº3Ši¶ˆB‚¨L#1g_Ž®®k¬…Ël³<µd&É7¦Í?#êCgŠ%sYöÃ#.ê:h9#»:Ûï˜Ô3ß.ž\ä´„Po#1#.©ÃÍÎdþ“òKÄS¦ôm¿|ö¢Ì'vŸ·»´ë¼½¡Î+JqÜ®ŒÙ–aUÜ$L¿Ù'±y‘Úöèvh®EÀ¯¤j&݃0sÙÍiý0ìlþù‰iLæýs\»÷ØÁ_{ö8íÆÕÔ"iƒW¼“]㊮òú¡.yå(Ú*¥9ŽŒø–Âl©Äæ)<Ì.“ß6›ÆÊmÊ8’F#xEqˆëNz%o·Cצ-¯§M7¼£œìDwï0¸ uh\^Ñýÿk—ÛA¢y.ƒì•œ’ʈøû@µ³-dâý/y#/¾²h’VœiÇ#/Õ®œ«Qy¾..ˆ¾0‹ã¥Ò0 µˆX½d>x'dw¸M3ñeù=4ûìéçGºoù.»£ïR)F=væêÝWI¬ø›·k‡§X§L}o9o_–¾½ý[„-¶ë5šéZ:š žÏ/5\ûyu_MŽªrÎG®8Ø¢‹¯ÊgN汨ž.+Iåϯ«õqå|!l§0õ8ìø^9b¹~ËÂ8£…êb 00ºÚv,´`O—ãg åßekì%¬óÑŒV1‰‰`’=“b‹úbc³¼ë²&l/§¶Y¬Ùó¸Ÿ!}¯†:¯Ÿ&Á"Bf8ÂVñ~†ÍڱȹæÞNtÖª_Ï´é–Ý0Ó‡åB¯\ì^0”¨|N‹yÂ6É„D ¤ÝÊŸõ qwAÐ'óÌCcùï»V¯×¬tþØ›Ìæƒå϶!}ݯóëæä*É/h”•.9Á„×îõÌþ'M iqÏvîq>°ãUì^Ū(.<¢¬ñ¿Í<xï㎉c®û»ˆûç×çæûHà‡ÏIñÖüÔŸ9PŒêç¢sµt’)¼k—ö#1µx¹…#.èth‹¯¿ß8ò83Îß,8¥ãD<¹îÙå’˵ØéJõ„öjâÙ¦Ú„öb<x5-‘/sšhHý®Ð-ÿoúÜs?³!xïóÏk‰ïÃK–¹ÃÁ¼„[y:E°ÉÑŠbSƒZ+ çO9Fä_óÖDä>T1ãO&…œd·–°ûYƒa“ˆÙ0ûå½Ö™„-‘ßíQÒñT½u’›“¿…#×òÚH;ÜŸa›'\^ DõÅú5W5¥¨å@Y†W·B…aF"eUÕƒ—•µ³´Èˆj™ƒK•9׳ͻ{“\˹Ÿš7{¹˜œ¼Ôªø¿/|ù/ž¯|Ïe}¡×V,zºÛ'/‘Û>Ò#n¾{:é@qÐà†òéÛ¤»Yôñ“Ñú{O§$Îá€#.h@8ëh.G¯²à*;Pnþ›ut\ÝõwYñê¸'g.Ëí¾äÑÖ¨& Ú]ˆÔB—²)!Ÿ©Ö5|^7ËŠö[ûÔ<M ú‹'luÍï2ÑQÒõ<½—zO*§]Òm«Fø½Û°Í!³R{ÃZèrúÄ5b–›™CLè®^…!nÿ’ðžÞ¥àµgïx#ç°8vV[GVvÌ9±f#ªå#/¡OèX|PŒªýjcÔ?AZ@¤Ã;Jþ+ ‹O»œ©ÊÊø(ï.²ü ï´gšbÙa(˜xÄ~¥áOæýM¦0ï3f•}ðöcò‹–ËÈ#12Žƒ xé»jCé¿ð<û×ä»——IBPŽyhK¢iV’Òn~g—ñŸ$#1ïÛ†"ŒÎ$ÁËŸkgK3´ÌR³jUö‡J“†ˆÇ×±#.’„-vÃ+5@Ù㙆¨›2…Ÿ¯ÎÊùF¼Œp×úxAüä€x.oË}wozwDôñèû§ÆRнܜJ#.ôª|Þ•#.hýéû÷}@xúâ„‚ZƒoÖo³”}ó„ªÂÛã«Rõܨéøý˜Ý5Ì$LOL=|ª$‚·‰QÙëx0h–L—ù@÷qª—ûççp>…´U$Š##1aCÏã’‚ˆ±)¤Û&Æ5´kW\ßÒ¾ÞS¥]ªzFóQÂ@¸ÃŽ´5Uþß]Žq#88Ou÷å§Kšz£ºéã\O#ð¹wHpUH²FE (1X ¡óÎëâ[9ɩ妅H’5#1j±ðµ9 Â#1CD•/ÑJ±Šˆ‘ä*áPæÚ¨V£”•¹0ÏÔÛ§òo¬1f3üƒû³¯èñä4n#1)`ª÷^ÑóTœJ:D‰}6½áùÐýã—@ˆaíXÙ!Ì´—¿Ëö/ð¯åö)[Y<â#l#eU fœô¾îÞz±ÇÊn°Õ…êßʦ,v9®€ßM•G¦a²½ÁX{À4¶œUNh—>¡õ0Qïºeòú é»ç#ï¨0×™ˆ„{¾’ûu‡´…îaR"\wëÞ9a¡5Ü¥A#Uö|€ßÁËÈ6ŸàÂß¿ÊÉú¾`4ìt,SÓcp&9d6é©ZK·)v4‡/š×<<£*€,ô#/årJ>ø©rI#1|¤‡r#/nšn=Vm’R”DЉl(¬hÏÃѦ†l8‰ú¿ZšÕéCXmN’[&ù–bÏvl†ù:—äPz0àÍD§öïËÞž¦“›ç\+4Ufx Lú°Õó¯Ù§Ù÷’®g‚âU¡ç±,#\ëhrsr¹B9~—fÊ#Õ¶.0Ø/óv¶w-¢€qz&MpÌ/yŸ7 u‚Aå¦BÐßáúÿÑÖw¿ŒÕÆÀ¾¸áZëÐ#.v`L6ÑáãCBÞ…h߯rt „B¤´ŒŠhn”]È°´#/þ•:½ŒPËõ¿j‚’e)Î膵AI7C§åþ«âÀ¦#.†Ôs$”„Roº¥ª*‹hênQQIcb-ü¿¿ÖÚ÷~F¢R¸N…ÍX÷°Ë5^wXtͪ´óe ntÅÀ20Ý×.–±R]Ò#Øt€2Ïae§eD#/–Ø–Œ]°<â/º!”loîç®Ü;\íe:¢–ÐïŠq×åþ‹“ɧ|U¸rÐ~KÚ…_“'¹Ú2„‡ÇÁíŒnáݦâ¾ÅúÜpWñ¿tÐ}×>^ø!r1h‰s/‘#걚ÑtÌ:ð »ÝGšÌ®Gúzo»—*ªƒå¶P·›;(ç ~ãùvîh°2‘Ef·}ÂboTY‹#.´:½VäkLbŒ[%ï<´¦“v#/Щ}Í·{~ôúá+Òõð˜Öu(«Q#/¾¾T¬pÎÿ¶±R|±€¿ƒ×ô,IÍÇ68yfßo×=ñ®úÙ¨¹“ËJê—¼9ØËyrÆãÙôÇÖz㶜™yºH‡}‰¥3øL?‘ï¼PC2P„!D7ÓʶçÍŠe§~u4ÉóSä¡÷ÓTOiRî™àUM‹õnz¼#5Á¢(ܣ܊z7½9ú-µŒ»¡aØŠôULÆ°¤.{x¾«| Ë‘%Zꓸá;oöÌ¡¬ÛpïhÓH< -$kZ™^òwulôªÆZqe‰±4Äù€lÖž¼4Ž‘Lƒ`<H£Â)Ï<ùß~§ú#/Ú^ò+Êx|EA”¨C±x0Εº…{býKCÛ0Ô»µë¢3dŒÄ#.è`:¡cˆCš2‘ ¹jŠÊúsv÷6arÿcÜ ç¸#./À®<÷»ñ€<e‹Ÿmß;º¤àÂî.#1oz½Øó ]º/N:<Œ#/Å×»ù• ʯ[ëŠY!ñHB’B˜fÎ×[TŠàT0LÕäA@êúL§oVŒRØ b ™ÏçôXSÍqûå4ˆšR)7hfÃÐ?ï~ñÑïŠ=Å°Â@É—àT˜G¸¯hT¾´TU&«Ö©PY8 ªJ:×=ÜÁH3‚ý#–ù€õñß²¥tó…SÄ°·0èÉ(û¦º“Á„-.Š¬°Dyõªóùëi©'.©ª¡%,¢Á;½E¿„\3¢i¢6e¸å¦`“¸˜Á~R§dãÑÀŠrqõ&2¾ÌñC?÷3ªMë/Åõ¨Ç#øäo»öU‡Y9zŽ(éŸÎ-@&œ9[HxK«¾Žâ®W”tº„”9ÇXé‹ìÎÓ :=®Bé.m¼±ÞrûÞLôx^O<³t¬N«ñüqåÜ)g”Ër:õ÷“û’`i‚'(ÒEÚÔ%ú4:d]ùS±‚ë3ÐZ”ôã«ë‰]3{¶Ðoà·ñ‰d®’Á(fhLÚ²–Ù8:–üʬrû/37›‘få4D(ÊIQKL_Œ5¯E¯Ô…ÎÁçÉõh·nK}IìŒsK]€ÐŹ{z¦7«„ƒ)iƒU$»pí#/™¯s¼Îæȯ¿ôÛ‘¯Ž{]×á²Ëñ»E¼ ž^¯ïfÜò:ùÝ!çÉnX‰¸#/Ï¡0|Ùý80¢at¥Ë¯Êaò%=øÒúۨط8#/óVòGÛÀÜ “ÇÞü²±hñ?O+˜•»wF(N²‰&QàUœv¸AÁº®rï†ØɆ#1'†hí)¬¼Ál~ú[Þä tîa€rsæá¼XídRÁ*éÐà+Áh{m7Éîƒg²¬Y¡TÅ(!éˆWƳî®zz½´êÂQe^GµùöˆÂ7K,·Wa"á•»ÄÖ漎ɼy PBõ.Ýæ½"‡÷/:>6vCYé_‡‹àRß½9çÃBæÿí¨,›2•Xiêõ9ï+1™òƒd傤còóF¨›7##/ÖZ8ƒ{TˆC`&€(LÚ¶Åüs³R”)‚(¡>ñ¡ÌG[€ŒŸ@aj!*,n<Šs®a}Ù‚ë$–ºÛž1õ:Á}\Ã|–QèhÓª<ž³Ï#1Ç÷]ãç$_%¦žv,¦M=RZ"B_DW‡,/òoîGSdz´waÍqå|aÎýÍ[ÝôMTÄáóÝúñÒ) Ñ•ïÝÍÑiŠfG(2ºYÇM£¶qí×\IœšÏD²lá‚l#.¡#1Ø‹ÛË6Dp™{f¸òÇwØa=ƒëÇM³h×±r†{Ôª™XYDQŽ*"äu£žÉ[d¨¡Ä´œÏD.Fâ99—Ô;8b¾ZÛŠt»7·´ ‘ëŠüŠäo-ôÄõí4ô¬’;M>JR ÇH!ù”ZäO„qaSfÎ3H ®*sØ‹ ê0)O‘تè¯W#/½"ØÁÒME{\²–ׄBB£êúî÷ÀêŸ1l7>¥“ŒJƒêé[æÖ=£ƒ•«;ïË£zLI.m,Ü«Æ»æ•ìŒÐ¦Y8‘ò0à6QJ¹o¥´ˆvuÍs䉕"„ªcròzÎÜÅöÄeZüÝÏ+tÈHù£qëÒùo—tø~ÎyÔÑh“j iŸ²!ý«g „&Û\°”*¦Óiï¤9Fàî£ÌòX¤€Äá¾:ˆRŽ¡‘««nN*ª€È? ’rF–qç7$]܈⫉"ädEÛQ#¼xIÔíJœ(0]zrÆ]*g#o62W {["S><½er ¾êäÄ@5\î‚÷¬L1i½ðzMÚ#I>mŒ*aççÃ=¼ì¿Ø©¹ ©Ð[sƒŠGQqÛ<mБ#.Sªv íAc°Ãj•&̬S_e‘¯gIå6uÈÎÞ&ŸÛY4š8ò‹J”"gÃÝÓÅ4ûbK,…ÖÓRÌ(%n”«éfg‚ QÛJs*_TxšràöL%’×)Å4zîÈ B²Åè3ºÁ!sÁCjȽvÜuvµíÕ#/âMß¿#2û¬>Gl¾›34™Ý“|6e5©wêîB4ÇQ_j1³ØRPÆ*lÒŤ‰ò«p0¤Âuñ–Æâ¾YÓM׃\™ÜE§‚ôHžÍ}ⲎÚØàpYb{@üt4™ŒÒ„~^ïÇlg¦õ©]dL•J²@ÜÃ%#/CÖYg('œD®‡#.û\udb#z›; nŒi …S6óg[%{a䯾çî‹þBJª¡ªO?[£œþéAóçA¦È#>M^ÎÿEþíüê?¤ÜüÃË8FŲ3r¹vî§;u¯<ò¼¦)\Ûp5ù_ç~Ãñ××Üæü˶¯‡ð{Ë.¸¿ªªá?ÕÈT£B¾£ó>¶—ÚÜV Ç‚€ª©¾@ Aþež¤O#.sò`8Àߘz¶þZô¯á[òõý¿'©@Z½-ü#1ûÀ´ÚB˜”Š¹0ýÐ3þcøËŠpú#/Óù?`ËÌwQ‹<ƒÕ®¾Z¸7òl(Sv2ƒˆÞ¾›no„Štw«¯vàP›ékœh8ÝKsb€Rèlꣳl§tÉš}q)mÍ·{à%ð[¥Mñ¨’ŒÎ5À†Ì]7Ó0üý]npšöï6꓇ץˆ~UóC±0ýÿ—“f>#/0ÑŽ¬¹D;¯OIÜUàÔŸŒ#/Ì8|œb›‘)#.ñg•|,ä…ò¥•¨Ñ‡vn‡×³ÅQM³=ÛÙqÝɺ•Œ•% üœ¥ÑR^9Nž-ð蟙û{wäròvž¢r Ý’Ë Î½>!›Ûßžè\rÍNøÛ*âû;ú^NœVm´xŽíÆ“ÞÃSC›L´¡W‡eŽô™øÉÊÈä,u‹Ü/·UÒ~ˆÑõè £Ç«Çûì€ÄÔvpß×R§'’VjODþßèÁi;ºTðá^;Ù¢°ÑP`«ù‡Ì¹[‡ç™ñ?ö¥2·c³îÃw¿³¿lâÀ„=Å0¢ÜƒìƒÓ?Ã1d náûHþë˜w¹³Uì]è‚À„#/ÚúJ`#.:åþgxYsÌ£°o›F†Z‚8Uð(4@»œBÌ=ŒEMº#“}[9¥#1²)]@E u…”ŽÆèzp#,Ä"ml#/†Cùã³×(ºCTÚP™¥ÈEI#.¸0Öå‘Ø£vÊ(º¿+`PšÎ6êp#.°"!«dÕ(Ó€¼EÒÆmc0G|À°á"YDÆ‹4‰FD”! ±õèˆ*g©#1Ùñ4¡è$»Õâe?ßvÍ>ÐRÌ8² ÐC“Fqy(ç!UþŸÝP9ëð‡V_Ï¥_ïàÐãß4ÌVù1A·÷¡r²lìÓü9x$–#1òPh ±â#.á8³ãÓãhö†¡Ó[¿í~¦U¦%…p¸ s|%ÄÅJ’* N¤—c§«Ð·òÖs^{GˆšL•Þ ¤üÝKÚÿ‹›g:É+Mn_îå(°j@áÏ!ô}4:gŸoc’Þ9—bÚMËôAwB#H!6ˆÔ"„Q$diAIF#/¦Ëp0íÔÆ"nûv»“VAH[”Õ;7Å*;JíͳQüÊ{{vñqR¸ëŒ«üRp¾dSÆù\aGï>fÉ]RÎ*8šµá8ÑMŒTL°ú¯bŠvpøEŽO'ºÔF25$ÓÓÛVTœ;Ù£5F«,=j¿ÎãÙTù4Å5A#/Ñ>•áº¿CÚa°xl<ZvTT¶ï¾^`WÍ×mîž®iHø á&¼_³Þô£ØZºq|‰ÁÿElÆÈ,9ñ¢4v—¿¬7?\1xh_ÚmJéé!!ƒõÄ=ÙsäÜbYð,WÛù}-¼½îá654Y±´S4©f´Ì”Å©çà¾÷¨ÏT¢ªª£ IQ…Rxug囄qØ|_gC¹7vy4(å5œBQÌÒÁdå¾Òö黿G(dHA–È£D‰$5N°Nic((¬¼Kª—¡å`ZÒ'ìž5·¸óÖ‚‘“Ö#I Ûq3"#1B+Öm7Ñ`LúWH›5‚ë#/Ý@†ëR%¡5qtLD’CJt6_"´óÚ`^I¡Ñ2C`ò2zdVºR£4 3G¯fVJÁƒå¢]VRnÓ#.¤q&¬b¥¬å¦¶9jQ@NÝgo§ê‡Ö›9p¿³ÄÏ@œ9¯dè‹Ý˜mu¨å>NKƒ©"zÞ¨HzE껑ïˆg ¸3zn€ìœd¡õ°˜;‡OÛ¸{’àøíµá߯½á’¢‰äVº€›HµœÓÁ<¼hªæVwn€sŒÎDˆT*{âÛ%m[ÇY«%µy+xªì·kÆÚ®U“j¹ªéµF$`šmä왈˜ÓhØÇ©›šÄn½vó·[sm}2l”š0W¶tRl¬ ¹®&Dšà9‰sVjî°Ë%#/ÓZj^ôÀÜ‹k:E‚Žl¹CG<÷ÞXèžg‹†|Y0nwô8swt/#/ƒ±"Ũkä'Ë|‹*âA:WÙr„;Ê_¾u“#1}%gʦ†~ü¯¯Í"ûÛo¥i4%“hÖÆÁ¡8MƒbXx!qð/;ä¬ÑÁÙÑž¾;Mo^Y‹'‘íÜ!sÌC*þ ¤WBåÙŠ8=Pì®#/VØ ØDÌ#.oU$¦ZíP§$é‘ÆÆ•òÎú‚}•øØ ‰<¡3ìͦog5‰@“òí‹fsQ!«Š¢#.T ‚¤ˆI±S»k{ox–Qk×ô/  Bª*D‡uîÿãáZ‹ÇÝDãoâ“·¨<Qª¥V¼;0VMªz(lë±RdaâŠhðÁEă“M—CåÇ3Žvö¾#.bü·Â1‹ wÄeÃBn– ˆn$4I÷¯ØŸ^ý©Ç^uÕ>^sÙp•ã¼ìC€„©×—bÙäŘÔJ:ó«(R…—I#1ƒl6#.³üÙíÝŽ{ú¶x|ÇáÀ0§–š(Æ$:†»Þ”7ÞPk¢Es:^ñBéSaS·\¯,Ðg+ϺŠ”B¤bÓ#1E‚È(:ñõ¡£lN•‡B „ÜÕ##/Ý|ôº"wõVgóÖ°Y#/ãÜÁâ Å-yTj@žÃ³´aJçå41þù9ì¿\Sʸðø{, ÑSUWŸ’CãK"Äy!AV_°¢°Æ¤ƒT75Ô¸g¯//ǜݚ®î”Ù,At.cgÚKåð¿øÒwuꨩß2¿*8§Ê³ŒÅæ}¨n)òÉ72hQ2¯ÉßruùíKÜÚ{œÇ„Á*7 ñåS•%D=㸄°gGO7Ñš¤”ÀiDÆtÒ· È2);ž£' ƒ?ê&ƒŽª459œ¬UR‰è…òÌᲞ¦-M úQ¶Û°'ßÕŒ2-†ÖáÐø´–m·yçE®ùMV¼ìÆžæ¨I&pžæ5AÑ™=¡þoN3uuÍqӯźqàìN;N—FøyãƒÃ³¥‡>’ Ó¡¦sú}|MŽd‘ÉhMª‘P0™•7ïÆ•Tìß7Ø9ûjÏgݱ§ÛPîß;d–q×ÄQ^÷!ùE(ûûUÓátd;#/.”u«ója¶û£seÓ¤2àϳ+¢ý»ƒ€‘$nÕ¡¿!( ν4í}rÛ2Ü®ò¦ö<#1HtE.œÙüO¯OWH¼Õìå“ÒFärG#cÐÅê•}ØlÑQ©¥”îוâú¼ìÄ.‚—Ѧ"Þn›0xT%ëAÀô>|Oñœ/VzöQI§!¸âgŸ™i$ü·{C[Iô}<\Û)çTì‹Íé Æ'÷>¼ÚîÊIê·î¿W>íãlk—í&¤LF8 h\DKE”ÐÙIiJ²A“Òã*§…¹1ƒw¹×hOT†žÂ’;J#/:ˆ(PQ“ž×mP¢T»º‡r‹×>éP0‚AÛsV”æºö§ÉwÚÈh@;e¼žAØt»gœLdq=q¢Ž#1PP`ª‹ÈÀt’bZÑ)PDC¾tÈé\|û¤¸šv]sí,ü ¿¥_K®ƒ+¹øUïŽòù9lÇ„<ß}!žÍu¢,عŸ{æ÷ÞXÙ—ŽQuü9·ÚöGZf)¨õbÎgCoÇ{(2,37ဧÉlbJ§RNˆv·ÙBüõ®ø°öè`Øt6p<?Ò!ÁÛäÿKŽ¨øÐÿ®¨žµÌ3£¢Ù,~s‚ËÄæeÓ¿Ý7“Óf+øg•#1[¼+Ì.C½ZNlƧ€ÊňÄ4]3‰Ô©ÀL‘ï©T+R–‡â]ÙB2Ý9(( ²ÉI·Y†PvÞðœMôèNpMf\kèù}vuòÒÝ{¶†éBµDNê;Š°bö¤5OŽ´>&;ñ!ƒâ±ñUTãç4kÀ·`Å÷itbÍ=׶ÞFË(±¢ú1ýÌûTO'Pø)÷¸è”»o‘7îüdÙrDµç×v%šØÈwž9lboŠN¢•AÌàÙAÁúŸ¹#1ªÆFivLà7{V1ÑR„˜yq?YæN›}bÓý36öt6ð&ò;]óZ ? «læbÜ•%W!FÂÙ¿%¨HM¦ç>gdÕæ2ÞcgV-~ÎÖé#/æÔ9oÔ{Ž«¡È(îCV'zIâÀRÙ 9¾'pˆ– Àûc8·¶m£¹tØñ0ÄÄ3"ïµÉWC‹’IÝ ¬m±Ñ4Tž›ª&±”ØÍmUŠBÐ7aÁì4—V*T*”ft5»K<=³ŒúÊê*C®Gæ AïhœmÀä¥,Ì`=z~]©û¬-ƒ4ôôa¦Û]qO£÷§{hûǬ|jBrdk‡¥‹C¥=$R\€xCšW5*¾’lÏaoK)b"ï/D§#XyDù;SÛ\¶–WoŸG9fäîtäp#.öØt’¥I¡7Z"}#1ð5‰¡c&q:ùöû8l6½ xì✄:‡äÝðýß=#1MJüDùp•»î»ÓÙ‰¢}lö0ÕÕ‚Ò°”<Å£#1¿N=Ù®Ûkäƒ`!×(@4üØ8N#.DýÞ«,=»>k7¦¾ïèâ#ŸÆǦÇúnx#.e#1È ?\÷ℱ*3Ķ°¶¤ôe{*¾¢Yz¿“õè9™Qζܓîã·òÔö‚'oíÏ@Dâ÷Ù|c²&ø˜ºÛô[³ðøEñàm;èËœ&1³¶öBG[žñ¿íæðܱò~¦Š<¶ƒöÀðÂ}ê#/–Žþß8ð*¶<y~xj׫¨ò~&œÊJ¿>Œíyÿ³êÙŒ2ÓŒ„ ½s°ì‚ì^Ô@h8%•Pe¿Üz«o¹&¨‰2(Œ”lN¿‡èªþ0ð…íW‚ÿœÇõ`ÂàúÓ8€û_ç‡ÝH*$‘He\¦èaÑÙŸ£ûçß<8î³É9ùoïÊçúÌ1âôeðæ½?Ȉ÷¬F6ã?‚TèuÖ^çÏÑÏåç€ùé=ÿ’9¯Iì†'î?ÒOí•ßËON¸ÅMÐóòÒÊ ë¶U#..¿¨ÿ?åçþüy¿ñüËæÁßþ¬adZ¾[KجÔZÉœøEE]?¾0ü'M4#/ êå#/Z¹ÏR{YלiFAý`EÿÄUL\ËÆljœÒÊöʽ_¦h€ 2J99ôJW»Uˆã@fÒ±(ã6‹GY®Âp÷ŸÕ„s·~߯ó¤ÿ‚8I äc+¨œMgù¿É÷õk>}\âˆ?yò׿ÏÿŠ,©N’i ¥ÏÖoÞ¾~=q}õåéU~iÈÿwĨȋD$Y$Ûö§øK[ˆ´Ê/öîûÇþâñËömƒö08ƒ6ª'ûW,’Ÿõ‚I‡éö#jã¡'j$¾`!d|Š,¢%‹‹›vW¶á·ÜM)`ꉉß}Áž(!m]ç L¹GßÐ5 ìQ$ª#1‡qÍÇžüÚ6sSK΢>ßö/û\/Ϲ22樧OEv™§HÑ„-Ù{À@‚ÃGŒ8ïê7]Ö ¬] ¸–€b7}‡,öáB•¡‹[ÉF–ÿ¯/Ê|ä`ciNq´Áþ–æ—Ûþ{O°ý>³´Yßï²ÄRÏ…cÂòb1ÉY…-cÙu|oBzÈMS© ¢Ï ‹>ïÇ—Ûu.ˆüÊ¥-Æ?Gæ'^H }ÉLŒ8² (!Ä#1dJ^üÅú¿~ðëÿ›Ý³O_²»egj½Ûnç—ô×Õ|ú參®PÔjH?ô‘[ÎÃßÆÁD†áx¶~kЂÉ×1âý°<¥äyj ^aôg7#1Æ"$X¡?ÄåÙßôÓõËïj«Õ¾hVˆ”–·ÞÊË·ladÀ§î“ƒ®ôîæüã]ÞŽK|_ÌK‘×ÖÑ짷£Ü¿Œözþ¿ixT6{k_^GÃÞzÏg§˜ô·fñ×àèeçôgÑì$TQð>*Éç´µŒwÕ⊹r$]î_ÍNßoø—2 ß¿úJ=â&+`º¯¹s,Ïu¬ÊWø33‹ms6¶@¥=(ÙÖýà€~:ȃ”ˆsëüGŽO/ìô ðôLý¤¶1{ZQ0‰Ê¬À×îíÁùO‡õ|1<ƒHþ†=«küß?º|ÿ©@ù¢x#/^Ùð£/iÓÏѯxò“ÑCÏäSè jrèÄzýUï”F@£ÇòÆñ¨éΞ„kßärdD·?Ã…ÑÞxü×ê(Hèÿ>W£¿Mâ÷4W³Ž4ôVØù á#Ô¸¾)â–‡'¬5L\€@ [(iÇóx‚'¤ó¯Ó ¿V¼ìv}¾k¿Ýë7î2·ºán’_ÉÊñÎ8V$-H_$ëð¢Âuµ¨Ï×ðC;}FÈ Óåxêo0òy¼iS4Pû¼ùþMÎѤ«Õˆpzñ†²¦- Få‰ÕšÚŽB&P#/$ƒJ£Šl¡Hz'òú6m×!î¬O/*ŽWÓpçéÝ+ˆyùÕ¡Fõ›~˜†#1Y“çü~ZC;Î#.¶Øé_z.µUëB,ÑIÊ“ó|3NþWâñ¸S'FŒPioƒ«©» ×£›÷=#Ò•Kʨ¨—‡šûüׂ}Ërý[aý¤ª&¿cí§ý6ãNÞ_áûý0êß„…l!!‘ðYôÜÀÊÒù«,Œt‡S¥z/YuŸt'ˆßˆÙ'ó§K#1hœ`+²ÂÃÌZ~˜™„ÙæûÿwËèmí9! LÄBìRŽHåÃw#1Ç#/ñƯ–w/Û}4õùý]jºh¢º¡:øûnt¨Û±[nk+)û#'PD4Ï Ò+ùºõ öTÚž‘æöhÄD9=tši–°š®ø4pPOì½ÿ2så×Ö9Cow…xrï ‚xg*NQ)dÚ{WU‚‰Ñ r YjËjÚ¡î-'îd‰ð×ðmþÈñÇ6m!Ž!+DJý<ö$»Ìaq|¶JŒ£õ{!KuqâñŒ"¶3{毨ðÕ–•ôì•Ù»€@‚#1ôàG„~mÏæ¾?·o=¥/Mû&b°ùË#;BexÒ‰×e·yT[–kvès£®Yµp#1F¼,»Ãï (Dƒ[a¿Z×uû29² s;ù~¢‘ô¨ÅÛ›ú[í<¸2;È/Oa¨öT|e$|а€{Ï?P\=¯«„¤’`Šî0•ŸS©)à´¢»ÓxÔ.5ž™zúÝv ׎ßÓèW\ƒøëÀ;#.CÔdTQB³7‹ù)ÌÚÕrFê™Ç+D±³Ý¿ç±ñö2:ùd÷9‰ý›¶K[f… àµs`à à£H†Ãã‚IÂ΃ðoQgJx·…ßwû1¾'+qfx£;ã¾jª¼´êv1þ:Æn<cšæ)”—žÐlb»[×ÄùA·ÛD$¶]Pþ¼¾ò贑获µçTŸ¿½éÑ[ÞÞêÎ_ª|o™WÚZ; ÀTáoBfaÇE"(Ð’¤Œ‚¼J÷ìjzœ¡)5)ÁTÑj]\¯š©»›LÑ«ñ8©Çªxz¤<=ˤ¥0““fàð×È Ïý6Ež+PÊvqä€yG‹£ÁV÷†ƒè°ÇðÁ·Øùt[À:¤1µYƪ„‘Ê'¥º³¬µ%vR•{ù0›pšäD©<ê®…‚ôQ‹V#ÒÁ;wLf?ÙsìÚ”—\4ò>>—Ä97¡ÇL/wåFÇ,°ù‰÷ðôæÍ‚år¦²Å_7€¦ÓW¿’*°vt`y>È1o$u“±éf·ñ«û£¤3‡•Dlú<{‘›£—°6Ø«°ÔRÌ‹`ÖÆã“—Ô°*»ámœ´ÿ¿Ö%2Ã¥¥ñRÓ—Êü±qIœHå\ÏîÞ£‹#·.Ev‡m„âõèìˆuÏ« ‹¶y71¤$aµ’¶åpÆ«^&°6X×ÊãHÛ0’guPBȇWªn—ï›Ô°µ%ÊcµbÊ,f"MßP¶qÈ®ü®Ú½6ž¹qAŒbT>è …P¯šo.£»íͼ Ç8Á8ÁÇ”Q¹ù¹1;¾!ëð¬Ç_’œ)ßÊ(ëÓ\w¬ëµÎ¨zƒè?\f³5`‚«ÔW“¿o<ÀhÅ¿”úgíÝé?<PP 0ŠÅo{“Œ]ƒƒ&öX”7;dõÊ5ÁƒI$ÌâZ®‚½ÊAÁV"ç%¯yRÎTåÑô;\gKŸW¯…mîGÉæo6r‰—<!óñhm;ª*ô»Öq™\¡}Ë©î•AtZ-ƒ4[¯ßã1á´´¹ç¯µWŒºHý!‡QÎm >)ËC"U{?û5tºÕÛÆä4Ë›TíŒpŠò?yÒ‰Þ>lÇÃhvÙØLsp#—8·_Ñ4r‰£þ-¡¦QNm(I~äE¿hu÷5ªNþšyTB’•êÒ&ˆ¢Wì*ž—шTSãaÌΕ”LÎ$‡7íöYøXé/ßÓ»eüøàÙß«‰Do3c&¤ÆþŸ™}:æ"Ÿ9elQÙÅÚ¿DÛáž8Öümžó¢ØëN°ìbÇ…Êzž’OƒÆÅ#‹¸¦Ú>¦r]éÝ;¢œñ„®pÓ¤¢õE XªDJ‡æzgýPó=\”[J#1”$Á Ǭ޺üè9Ó˜Xuþ%$§çÏíÚ!çx÷eµv©wD@‡&~&;×x)Ç´‡šr{ô,¥%Â#/$pW¤¶¹ët‰Œ{¢Ð#1ü8g€SBv,4®7#.ýw,!sÖS6º|N,”AÑ7·‘±|)û¾ŸnúÕõÍæʮݡëSXE2h0¯tÅÔP÷þtS‹64k½/ísξêȺ qÙEÛ¯çØîFÊXX…Ü_i«<ʹQJˆ”'˜è© Q×ð¨"G“·–ÅÑöãȬóöÃÆmÝó1ï÷ÊlŽŸ(Åšyí†åÉ<¡îxãˆðc eˆØ´:Üèj2Œx•èUÙÃ4Šã#.ኬ ñºptb›¢Ï“€¡“‘_c=Åuã’0û,¬SF{kÅé~Ü0F1ÒêöŠ]ó~+Gå"½ÉÚ\äØKk·ì»‰¤¹m¡+ø<yû¢‚Ï1Ùá—w û™±rÄ©˜5Ü%<'5e Ïëï¯]cßò°uX¤Ñ¶˜ ä^P¾²ôð|á‰Ï»R«JÇ?Š '7æÕìÑû'F¹ôµÊúÚçEFè`Î3(¾#1‡÷ùäƒ}mës”W-•dŠX¨$#1.¬ËViIâÉK[üGŸÐõ:w÷vòFØ})rO¨ìMù‰×Ê]½Óó›ÓȈMKc±öÉ<?ê‹Dzæqìod”ãð½còmSQ³#²Qfÿ;'È1a-‰ÏßUð}¾¯##1œÔ„#ñç˜ÇÚsF#1É[@SiŽ³0XôÄ"vk‹AR(-#vž-ou6’˜;Öö¦Žƒ8³µŠnØa!þõ¤Bò·s˜å:øbѳÊßìýæ\4ìïÔ‡â¨é§m”»ŠÝÐeæ#1Ý_YŽ]¶Çg#.U¤¹~#åú§¦tt%¡výlùâ#/Üs?`ë#1|ÓÏõχiÇûò?Õ0êCLG†Q‹ÂvÄ”(°UÛÞê÷mhI(#1æ^G<8ƒ6Vy¡öpƒÑäïb> „Ê\:Pðä»Ìôû8ñŠí“\x‹—²Ÿæ‘Æ×MëY´Ú`ž™Ž‰Ðß“¹Ë³å2~“zƒ\fÄþ¹ =½»qüŽ¯Ñ9vÆ£‹mÁ|D7Ǥ¢©¼]Ò÷¥ù@Œ<1IŠpq%ä†Òï³ðÿ”á´ó!òAƒÛ©qÙ4@ˆ»‡s¢vƒ!¢ÎãýgØÈÓ&&\ÉàÉ(8Ž‹yÏËçã$Æñ‘´«”=WA ‘Ö‚+ìÀök«&~´u†{§9Õ#1Í4FF›?:?dxÙÅøŽÌvÖ !}pä&=“FÖŒ!ÄaWZ2"z³óaàå4d-'ªŠ)ýNAÄV)%&ÿ‡[/†—¥WOÇŸ‹k-kïsï<n ƒÌ‹ƒ4ïËG¹Hû½*+€^œQS‹CŒ3‹_T`© Ajô#1Æ#.1¦©%•´# c?îFÀÒ.ʬDLNÚ¤í€Z•ôvÖùð,ž“ªÛ-Pz|r£ìäñf°p}±øªµ JàèLÂîŒ\15‡Ï_lxã9Fï;| ÄÇG0ˆže ‹Â<¦H[ÅS9-?†æ{Q<‡z?¯Käé/¼i|Ø ¸\/ üñÏ–€¡#.#ðûG¡<c5+‡¼—mÏ·ÐâO…Ž%¢„çE§MßÚÞYä¡ÔDq#._³÷˜7~_wó²`™ž§©#ïæï#/„ɲ0$<£ù®é,çþ<“ŽE"ljIüäÉDŠ¯ÝóL¯ì<£„×ì&~Ѹ§éŠk%0'Ö¬wŸæŸHøtPcÎxö(ÈJŽo#.RÞtrÖM†\OVJQ/ô)úËîÿ Ým!oúLY@õÀÉÈÕîûÏ÷}Áý?ä\ÜküÒ‹iOòÊ)ò8š?œk(¼wgÄ#ˆòyóäNP9’`t; ÇghóÙñßÀî¯//×="C~$e,zFîµsíú¿WÅ–¬éÖR@cÐHçèŽ(ßiÒú¹ó‚Ó)D§ïSÎFÖ/yN-º‡IG/©>?©=ŸçøÏ@¤ãA™žû°áAÖ]j(ÛU ¹¼Ã”|Ñ $’þ’Œb5÷Ÿ««ï hû´"!ðX¼ü;ÆÎ0Š0Gº§€œ}0©ðʱ!áôÑíœÜõ {È1:’QnöÈÚFÁõˆ¾§%ù‰`(IO#d/†¿†#1Îû¤#/ŸV›H3B?dô?IKõ»Ë²)ñH“ã@ç*1"ÚëÙ£½ñ{¹»zù× ÏÔòI3ïÉÅÂ".«4½UêÎ\3˜0}÷ípµ²Q˜±e‰zÜÍ`tÀBqªäôy¦r>#.øýØ+u·+²J#Šp ;™4ª±y‚ ;°X2£ž ¦iÒ´×Hn‰Ì¡JŽb& vß.À Ð@y ·uƒŽ|+·†òÙ§íÿ_éÛËŒÈ×ON~£(K¡"ˆ'S.[ ¡ÙÙòÂë˜\¤=0r㩳óO¬_|YhñóÌQ¯/Ë;¡Šî?Þçhv+Á8ŽÄø×6i(«ëR|‡Ëv#/È4³3aAr #.Ž!4‚+1‡ŸŒÄ—r¦Úïƒw:Ã13M¤ØX"æŒùâ·rÑwPÅÊÜ€l0œMñž%µùhŃÒmY Œô²kôw4 ™÷öòºwÝçNXiÿ#ü+îZ09þi¨Î#16Å×%³#‘ZBÜ_«–j(±ÅËüNŒD «·¿jçÃxçᮑåäųÀ{ÎÌí^áDoÊU©Iaß~A.[åbÞ‘8ruoëç&ûÖ2o‰ã Ç~Z›™¦m–W\©<#/û³#/5Üœç®Võ<e$GŒ_G^eGßäGNgüêÔ笮ic„04Ÿ}Ò)Äz%MJ¯€&n‡•Vß7šÛ]nŠèÌ¡˜#8;“šÓRŒ" ‰Rªst3µ-rŒƒ’³»oža$J䳓3’â ßmÁªQ€võ¹Ë[íÇ£ÄlöiÆü|Bè¦á¢8pñê€~Œ:î¤ÛÏdÇm·ˆìà,Í ;dŠ#/™¾yñÐ.äÚhÄ:øE²k4˜H‰ÎÔð¤ßVnh#|ôcž1ì¦{/ý~fÕ0H&=‹ñ*f`(#í‹*T'ÛÕ`¯„þï<bZ9U:r¤ïˆòã´ë£]x`’)0`ð½o)W(æõ®ڴAUÓ1ÇÕÈ@Éfƒ±ucXaû¯ˆw:ÿ‡ZtŒùÞ/).1lûh~ÏÚÃÊåÏQæÞú=3ÃÛèø‡·gñ‘o[r9c´%_Ù4#="P„Â15E{ñdRl/e¸õŒ–rÛ`øêÙOæÔÕÉݦÖÅ•øuÅTãGð„æuÆ4.΄|ß[(Ñz‹¦ƒØGòV}—ú‹¬ûVAÔ<›ŸÒOÝ:ãŒÙÈç~dêÚë¹FrS0Í¢¡•¶ç¥Þ²-¤ˆ®2Äa)38Ò¦»m㮺:´¬eRåÝÛÄá–nz´tá‰n‹)M:gAÖVª]¡#/ËAü·:×Lõ¦01_>ˆ'Ònò9rbVÍþžÑæI´åÓJ>ÐoR‚G3}.N^N¦ß)Xõ©fœsEUQîúâ<--çÛ󯕥¶ïÂuW_f¸—³nϹÀÒ [ šiKQêok‹œQŸj¹sU, §`ù({§êÉ0Š¢ÖùÃa7Y~¨ß•ë_ìã0gÀ]ꇺšàÔÒÖ#1%±¡ìå¹—T⎽h4Fa¦\´Ï¯#1:È¢Ù…Ê»›yλͅÔ~–AV–ΟžÊum¯EºÑht– €ÎœDiûç£Os„£ ¼>ìö—½ºDWâ‹Çª†yRAûz›žØ¬ó{Îyê¬åܶ>•UQU¬i$€&%P„1FÊzÛ„È!FV¸=ñ}Å4ðk^!†ÓWe˜0¨‹ÃäaHIñ‚9ož=¥WY4׺µŽekª,ä¸Á®b°·Ỉûu.g’;ežŒ÷X„ûkÎ!ܦ]‹rÄ’äçtâ[<ŠL¦|m@Ýq±”££Å¥JŽF±t¹-¾B¾µà8|íºhÜC-â!ôsáDJ;¢Ókùb›pÝŒ,;#.‘ßD§ëŠ5Túµqvà“ÕƒÀØ8#.=f±•aÀ³+‘³‹oÇæ«%È_³óÛM§ôpüòÅß'rsëÖƒ¾#/[;q¢B»i¿èÒœ:zŒ¨ËBÕÕhƒìοé|®_Ë-Ë^Ÿ>0¿‡èµ¾ªqo…Rn5J>]•8g‹jTÙž_:Ñà†“;±6—G%Lõƒˆs®ÜË?á«”!ä{g—l§¹ÆË'PãðÄ[ïœöø£Áî‚ðœv?¸„ÅÃÖL‹ÑÛ<Íü˜v}Ÿ”Šõ¢ÂHæF ÝõïÑeOø.u÷q†.™ÓâVé8/~™Ænxu©Ä̩וÎÙeb#1/6¸ŒÈí«ÚZgç³k}ß(tµ¼5ÂÔ#/ŠÀG–á\ãL…¡¦ÛÚ¾šÞ=;ªpyÃTXí´Váø¼ùú1º”Ä9@µ“¤Ç‘ÛùÌ‹™>Â+Gí¢5üî-ÉÁ-:<û˜Q!1ŒQ4ì#1¾·[OBÐ%„d M÷<œƒK2ylJúkÜž?.»õõ5”K`1¶AØþ0DÙ—ìÖR€Üsúûü*<V‚``#1QÀ*1äós)š¨æÓA#.ÍØá™QÄJI<¶^#.N¾zÆê'“«Å·v<‚˜]‹œÚHAȪ6#/ù9œ)Wá«s–Á¬4x UaKáyÒÜîÔw3i´-ªò/ 1V€¸XšD¹-»ßÃSC0J‹G@q™*ý±Ñ9É@5µ<šXF{æW}¬ÛKÀ˜¾6ÌdøC#/ô£å -ÀOG&Çwø«”ÄÐ2QÅP6@h[~a#/Y¦üÜY<Nɹ€}½À]ËxHv9gŸ{ndë—ŠIPÔ$#Ùwh»n<n}í7Tx:ïÉÃ|(*f7gsT‘$p4ìªÆÁÔg bø¼vtËgš&|8þ½°z*®øÍ$¾þ^;××ã5á´#–[³?±š‹PIGe“´ç)|'*w_vs…3vØ{A0ÌŒ[†kûÃt{”kžÐ‚€ê,Ù>ygˆç(f0”¡Ë—”ŒõKCµÚ±×xû_´Ã/sh•ùhÜÒXŒ2ÄIÖde²WÓ%½r6g(ÖåÎoe)%ò-›Æ®eó4à6é;tÁQ¿7´rüšÕržSµ4ët#/¡¢<OŽç<¸‰òÞô:#1T6Ûhhî*"SAˆÍ¿°S€Þ‘muo˜¢ì#x“^ùœxªãâÍ:æ@ëÊ݇`ÉàQ´Ž¸4’NÖó:.¬oÅѲŦ:Ø[ƒàt lŸ¼ž×ï©]vc¾Óélânˆ=c˜c¯³É;ý¨Úz–Å®Üð.Ö÷%Î"/˜Ò9º„ ÅPH6®ž*,}P@„OÁx÷å~âMz˜«w!ž•'zës]býÊ#G¦f§:ãjÓKV»jàWM°ñX®5è™N^gíA<Åzd§“(€½Ç~îL+EMÀŒ3ª(]xT=ñäm§QÞÂ1£#1‡#¡éÙ¤š˜‹ƒ(šH¦"Ý"¢ÀI+ö¶v®˜ÄN¹—«8–Õk:qtÇGZÁpÒóõ»G@²ÙÖ³ró))Ý2PÏäYtÂÁ¸ºÌf#1vÍd ÆëZwBªä=]âlð¾hö3•Í"z%ª´t2¯¼[¢tÙÖ<,„:—+ð9ÜHaqVª1•1}£ i«’ëEˆ(+|ùº{l¢>‹v-„œçè("‚:$ZÑjˆ€øž hÓM×çÁ9Ú¡ˆêe¾¦SyIíZó-ó7; r9±XouѶ*œÓ+„sîîÌÌìÊ9@vžŽ0²SËgzø"#1»‡Û.wgó`vE¬6ܶô#.d ÅŒˆèé}ˆÚÖ½¶J"ÃX¢ž¬vh{X£‰¶ñ„<A\—Æ(Éθò½\½NÙ(ÉûV.!´`¤[zwBè³ñ¿ÃUCBóÃ’$þ#.Êeµûñàëa ÉFNÎÞ"¼4ø†t¡hñü:\î³&Øo-‚ÁàJÜ3ˆÕ)Zäó(˜ w*Ú¬p×\3Žlû³wçÈèÃòƒ„ ®«Î=˜(çß6m·ç¶wͨãmœõ¼|,[osÅ5œÓáëuÉà–FR-Î>M?(&_¼±n!.OYZ&Ÿ:†/§6_LŽ|ŒŽ~íxž5ëPÿ±q5¯J¯C·~†©ûwx¹9]H-›% F‚zŒBQlÀ\ÀºŒl#/«8·#.à—€›ûœäJ#/“ÍnE¼–²åg™«bez-áÛo*,þû¡kêšÄT_³TÂý(Â5Õ«SߤH\(Ž€Î-9≛QAt†êJÙP,Z[^—¼»¨‡Æ‹ÃÙé/ëïN`…"¶®HÞ1ù$ŽâÇLxê#.¼"¼xò߀ˆšŒAéTs%ÀDÄ'p[@h…¹ù¹cÕî.ȃ2:»¢B›é}c©žÍÝï²ý_Öð¹r{¹è›Ôhzeyóæo4D çü©Ðwß¼INüQÊ#.<#/0pEÁYW3šâ–(6OÛ³—í÷zÇ»È=¿Fqs·Ìåùûiò‚ÕGÑ¡@IõYè~oJ¯EQ÷Yk=‹ùEè)"•f¾6]€#V…‚11Æ¥Š4;}½~îÒvò>½ãAÁ8Žñ0+/wXGÁøç䥢Ø_aÞ8€IýÞúÚžsÝ°på`á’}|ˆ»õ˜Ý§+FŒO ÊuÌ®Ëø/Ou½}Y·þáð,ÿwPð:$1ÍÌö‡˜¼Ü>ØZжºY`ÏAAb'â3 Aøœ0ÍŠéÄA 5 XŸÍÖÎÍn_'Ì'*¬íü¹pþà~¯uí÷Àm U;J¤PE#1ñT1ýãå+Ô¾Ïð@;Ÿüñºa=€ÏœÔûÐÀ/æ@T†ké²3†ÄæÉ´0N*Oó5U#1Ÿê,“ù‰JXC¿üæªúb<ÐÐHѸ^AÀ%AÀj™#.±PŠð-`a d;ZÏú˜ÿamÇ£M¯uŒúi#1n÷èEÒKÿÂq²QªÀÿ[Á°\ý!›äšâŽƒgÓj,/óÄâ*lTLÁÝ'{p"#µeÍÛÈV~[Í•}„¹ž¯n£"v]¢Ivu£{êU®È¿:*MÛ¬+vøQ€˜ãÂgPfd#.;û@ÙäÕëíðxh09?cŸ¼;‡m0ùÿØç¡©}ÿq¨ˆêm††eþï‚< ·€Ÿáýð¸/ˆyÚ‚îO¬žn¨[#“ܹýuÔÔÍ=(Ô,~÷ßø”†°$d¸ìý;[¹'¬iî^ch!¹ù¬*&_“ü«ß¸ðñµ®§¦ñ¿œ;<ÆwU~Ém½aŸ$d3iüS»ñSÐ#/X€L¨W¬õœÍË2ä’ÄÓJ«Õì‚ù—÷æ©qVF€#.Õˆg Q+Èì׳ú°ìõüß)_¶Û‰9þ6-®û%ºî^’FŽÿ_g§ç–ìþ|_¹ÛöZ´â}Åi}þ‰×Ö&’¬–JªÖK~t.dÀül`¥ó¥¨>Óh[\·gxßÎLüÀD_C0fAt*A‚‹Ô¿«ô‡Ïå.îÃZ@¨<¡hZÈ]‚Rå:±¿–m¤néÖ-#1þ†âT#/Ž–íádØóEBD9sܘ4ïQX¢ìA:ex%vIdÏúœ¶°)áÕ%]å}ä*#ú¢vziªßfÐ=EÉ=ß/#.ïn v:ƒ±î*=Ç@0¼ž#/»7-ŸåðeM†ÑÛ¹ToâŽg–ãCÖk}룈7RÈK#81`¡"]‚u'=}H¬ø³ÅiJ0%Âtíâ[6FS˜s¹œ€ì6æHéR¬k¥³&L#/ª®88¶#AhUðý >¨?[ì{pEd¦+Ñzö#/¶I÷ý¡{“Ïü<ÇtÄ}¡Ua7o·–ÅáùÃLÃ$¨#ˈ`+ó¹,ÀqfÛÚ`¬dü#.@>9òy“A²Kpßÿü‡ÀûX` !•R¨‹Ö!Á»â•„ã¡ÛYu{T2¨õ(Èè±å*ÿ†Îýñ†Ã>õ3]“cÈLD#.’LŸ|ùy÷'aãIJûáç@{%WÂßSPæW÷Ñ»m»'Ï>ë< Éžq†èÓ#/µ‹&2.úº¡¢CñÆ–uÇȇPD`ÌÇÃîAB#.ð14O’à1õ™*#ö!Úžk#.›D§‰úŽB ‚#ÚiÞ/¦ÒÎÓh$£o<‹h,#/ä´ƒ$ø'ÃÌãÖì9ˆD4öÿ)UHqÈò‘Ëó"žA%S5n‡UΆæ÷dívƒ¤M#.èS‡þY¦cœÐ6ëü«˜Ë»;ôŸxøÝef¢3wý‹)&ÅÚWñÑLBÐdtšÔéîÇ)eÇH6-ÑŽqŠÈ"$²ZÝSºI$±yˆ0•Šùwí\#Óf1Ë{Î!MˆúKžêÛÇniù#/ž³^/^VÖQ§¦aÏ€mz‹‡a;Þz#.zØtÁ„v:›èd#/!»·OÇ[2¦áˆh¶ÁE›”SŽ¨v?߬ùb|Žw2?ƒVû˨[û9þÈI‘Âb$ÇýŸf¯Gâs;HTŒ_ÙÞw÷—À’6§ùdý¯Ò¦QCLxÚ÷0yi¦#/}'ÜCkVŸo%W¨ Üxótvxëöµ£Ó‡îÆR~'c Ãd(k‡ýoéþñÿ`tsóCõµÊ0ꯑáfD‘ährÉ1IÇgB—fÊ‚ˆ˜<•cj'Â͸ÐÌí!`'_ñ–4T?q#û2õ62¥(‹#H‘„2æ²Ëœæ‡6™çžH°[[Ò@Pôb#/‘Aµ ÑúøíÜÐ)#ƒ›À£åè“B! ŽÃ 5z¸±YE°uìû¾ïùóCwÜë÷öjA ÑCLŠõö)¼àšÿ#/@ž$CS„‡˜‰Ñ“½naŽP²„!Éã´2@Èv=\ZL”CsQá1=LÃŒŠq/²Êƒ¿Ò‹TötÁ_\H~1usN*Ö˜ôÐÇb„$îR¥.Ò:jl$ AÎ)#¦Òš6)†=`DÌͪ†e›º‘ž <<{E<ñ÷æÑFÞO…@Xn5¤I®vÓ¶I<C7a¬:»Ñy…Ë#|N¾åÒ¤6š,ôаÉ#/gsMêz–p“D%AɈCPÚI"75,A6üîìþGñûŽáÿny~j?$#.Þ…ÃøW\”£#/·\2mâñû>q¯_¿ÃtõëªMèÁƒ2(H¿‚¥61°l¯êY»þÑs¹á’ÒïQ¸EŒ¦h•ÖFÖhÖiëL‰ê#/íÇŽH—X7Ž²Ì®R‘VMÚëÉ$’LÕFšÅ&*fµIikm§™«š²×¬Ç¦V`ax˨c¯X@ÁÑàÛÞðšÞ‘íIw#/áÅ,zÖ:²ñb4leQŒÛm5þF•oãñø©LdBBB pîüsRÈõÏ'ù¬C+4ŸÜLnG·©Üys=þ¯U½‡»fcò:¯úä÷×·µ/Þà̾–6"…@ûÁWÕ+¨µîõêjéŒó˼ÉI~]~åÔÚï^äûöý¨s§— ÍW‰h}Ä9øÙП•NÚéòµU] À#.;P Gï7ƒ¶ø¹|ƒÈÜ="’€¼€3d1š<mA0 Ý2d㻂¢Ù¡RWz»5ƒ”¡.õüXc«©Å8•¶Ûuv>Úv6ggÛn5ŒäвV× $“lCÆàÁÕ””Övè™^ù™x”OƒæŒ\#1™¬%5AI^]#/¡vS!¼¡AG1DZS÷k—†N±|Í#1ÁóË—ðÉMîT45=aOî6,Øâ{C±õ£¡tëß@—;øqÞFÌt6dm0žu÷_]8ÍÖ”®X£K›¥2›F‡vµ;¾eï œw ìíï—xð#.HŒâ¾ ¨V)ÄU#1jrüŸ‰–G-ÿ_…"{||ZDIUãö¶ß¬¹Ð’œ, ÐbÊR=#À¾ñ!uûväѹ°>_Jep*ûL \©Câ0·Èê=øæí¹ßñ*š*JR‘DS=N@Û#.|Ýã~$በy€ø*»5K*Šƒ”¥,é@)‘ú¶ƒõs+N´® àÒ»öú[o8q'Ñ…”¸4U˜~“¸¯{$õ,BÄkâ¼Ê£WÝ«×W‚þžmxÅ(o@dï! Y„byñ—óã5¤¡y†ÀØéqm¼<O®àÉ€»ëÜRýÄÊÏ×», b)·Ýe»¡BÐ@#.»@=ÝžP\›œ}Žò¡|wqÇò=ºïÀFiå?Ÿ6qdº¿MЫús„•óƒ)æ%¾4#1´OŒTSÍ螢ÖüIR¤Âöûîܘ.þQ=}Ÿƒý>÷>|Èð:×õ¡Í³ïYaÓçÐ<gY¿&EƒŠÀQØûûï™Ä?9ñ‡d"Æ4äë~£¸ö„FâÖt‹¹"@ÿUú#.÷»·y§âqPëO0øŠw‘R‚$n#.7>ŽõD˜™¥äE‚)@öðçÒ¼<ÞHrw´d#/Yâlêܵu ¤xÏÆxdSi*w ô“r„8[ÛÜLPy‚Š‚1ØÂj6&LK~Õõëzýzó|Ûoïî°º€#1ûŒŠE6ùì¹àœŸ/ÃC{·‡ 4ÓÊSè^¸‹t€ëA›Õ×ÜsTùÉýUk}ßh£Ë‹´ƒéò‰Ï°=#._ O±G¨@ OaŒP7ä‹ñ“ÑXå1!X•¾4>ý9]ôl>@ù~__Ýv#1¾ËX9ƒ#1j•à¥Ô± v#$’O™àíâûÑ6ïýú¨UÊ3Ø6Ö#.‰#.}¾¯B¿"’}Ý¢CÉšqÜßÓÀK~òÚîýžGÆP„;¥cÃszL;[•mb"_ãˆÀÞñ‹‰3ùuNP¾'ˆ€&ÒÞ²èíŒ e_í©+€Øÿ0Û¡“у8K³í*¿¡qìÅ_ÕR|!ÂK#/à,+ï9Ûœ#1nrbM©azËPçÌÊï—âu:tøU†,¤˜w¾Ft³gÏĬ!¢Q&V;Än¦1ÇùfU>t·O; €Â¯î¡”đƉǎư«¢bF„¦_ù©Q9lìkP¨ÉêÑÀüÿŸïí{l«ÖûBLÚ[#1RÆÜç[~ù¥Ó)FLÅ®±nFÙ†Z-Î&K‘âö•Ñ6>Þ>ÔÝ×ÔæE3€q¼óÙ3ÆAdV@ùú %¢Ô¨ÛÝÉ÷§‰èt—bXÑ!¶Æý]§yˆŸ\F«•Ø£eR°k—~ó§| õ$>TÔù w§™ }9½¡Üƒ»‡Ö‘Póï7ï6xzÊS“á“/ΖÞàÚH(SÌ°Y•R‹ÜÔøy|îçü¯#1“䟪:|{‚¦4ú1$5HÀºWè·öûÊ€~M$¨Áª¯öè±eü7+¼¼Gñ;h¯+ÇžœQ3Ô×-”v|1rFã6¥iJ#1¡Þæ_¯·»Æ{O<'ªÖÁøŸE}“´‡HH%9¬/œûЉëQ-ý_)%é#/m†¦!ŸèG¡y à[³<fø#/ÛvüÖ¸©ð‡rÇßyÛ¼åù~gIø<¤#.SØ"‚,i %1j3éSÍ9p}ùª„Eßp;#/#1ˆ4àb£³Hœ‚î›™êðܘ g_¥Éç®ywç+Òñ<€²0)óÔáÕ8ös,Á0|ÁsÔ\¡JP‡°P¨Z6ú„ÁŒû%hÑVe’À~·–P9½Üx~—8èýzÇÜr%IG¹žV`fR`³à¬l¤}õ0…1ÔWræd‚ÃsUoX£I~„bTÇr’c`•jdÎ¥Ä"fëðµQ˜£ÂY$Að'÷¹€{ÇV€ÂÔ P@9QöË9FL¸ ¡E"Ô3†, ‡®"÷P’‚£“Š¤ŽÅ⨚°ý9 ¡Ðƒ¢m–¿©ò¦Ü?i÷ýLþldAgo9þÊúà¬5¤”3N7‡YR¤*C}J w0¤‘N—Þ†íÉâš%<8ÌÔ(úµ-ê‹¥BŒîF‚Ä1þ¿æ>ñ?_Pv§Øwu+{¿HÕ~Ú¤*H¤W²‹·Ÿ}1ájùÑ´“„4Àü3†È;QH€£FĦa•ów›o™£\ªô×7HÚ·d6T%k-KnëË̺¬òvñ¯#/°7äèû}Þ¯ÃI°#19'1ëƒ.ü}“áì‹ïà]‚‡‚úîº|™È_¼ kÕˆ"#ƒÓÒõ£#/®cƒ@,;M¯å5Öb´K@²ƒ£’ ·ªRF1RDDh€Ì‚`@³‚çá˜ä:ào6!D@¾Ê1È‹móRòz%Û^<ÝÜ{Ï]&¬$!.ztí8ÞwçO`½C„-þUB¥ëHŽ»:æW~ñý“ãE$çZÅ ÇÄ31ÙCÎ#.Ó’C‰Gq¬íÜQ¸ñˆVñÞÛ'_1w:ßtᄊü¶Úg€‰~(u#1rC0hÆ$¸ÐÝ…´…@Ø0ù½[Ì»Ýù(öÿÏfÝð‚ú<l–Z/ºÛ/<—Aí×"‰ªŠQ““îù{Ï€¿iR¼ç¡ßeOwÚû®Ÿ\r‰#1ÖkìÇ×¹£Á½ê•ÊBø„lþ„MŸZiiᆙªÍNBð°¥Ý &PÞ¥ž¥’U’Iwߘ>ÌlÐÍÒÖ-¼#/#/-`˜h3*Z1¶Bƒ}€ Ïöûžwm5ILYðªERêž?°¾óå×E/ÔE™{¨ìÉ#¼¢9ä(dä…óî¡{£ Bh‰·j‘© ´Ù€!‹õÓºËé;#.ììù}ÃÚT–›Ê+ˆm¿#/¢ß˜w²“œ¹ök”ðIhN¨zYG *#:Aþ„üÃõUÁý)ç-hMÙüüööHó—ò!Whï>0#´FN¡Ò ‘YÅ%žìC\Izú6pxÎDœÆSêhŸîû¤/ö«»ðÕö›@_n™Còçe[’¯#.ÐíÀÙ9XÓd¦'í!O÷užÿÀÎ0õÊk‘QDAô)Uhª=£˜Æp ÈKœOM"È¿#þ¦ÐTソ¡‚ V+#1’"¤D¨ø¡d2PöN+¤8²œ8á-@[sHb!ƒùÂ$Só¤T&‘$¥ö¹} õÐ{Ìþ ®þçË·KeÏPa#1Nh…"”°Y.-“CŠÄáwÆçXpðØQ¤†ç>µ‰±Ø?›í×#/³öBéχy·Š=PTágÁ^àüjd*D[#.vý}’à0¨3ÑØz{N£5zO«vÁìS tÀàõÀårß$…”éú샂ñP jìx‡¦Å/K›ìë…ÕS¦ÜF™{ŽA<C=eÔEdÁ‘‚¨ŠùÐ[âD#1ž#ÓÃâH#/üêqóF^Îhøî_(â*Îöå¡#.T%P"këȼ³—‘~‡†œXMÝT‚w1d†E!Y#s‘è36ù}°A#.ç=à„°$~ÒR1^¯—ãÚìI6Ø7û([ä… q3'm±8M]Ûb%ã=¹*ùS³3VJ.ÉݱÇ"¨ÝÁñ?½®Oâ!Òq–qFœg×wpÅãh²EC•J¤æMÓsEnÔÅ->œÔ°:S5-aB#/KåÜ#v/AÈÑ{Puϸ9ryA¿h Â,á†;ûðwœd÷|é#½€¼N•Úž-œ‰r(…:An¹7GCØáÄFi6¥Ôò&B¿ƒÔºô˯“´¤VLà.ÔÈ4>Í,H#/y[Š$öÓþO,qæœ#.œv€@ä<ƒ‰ÁFÝvcoV&Ä{·fÐô•#/h~Ò#.îaâg&ãHÄ„t$cÔA«ÀFÑ,1@)X'3Ê+‘ò„õ¡êCª!<<z‰d,óÜ›80H%VEíkò-œƒ{ádz‘\¹NyïÜv—Fa¼Œ CòßZ ×Iæ€÷bsÒ¼ô®þ#1™ï£üôwŒC‡˜ùØÈ ¾NÏ´j%sø#[ °QK—¶xÝ»°àg¾(ìûÓìýÏÜwÎuü#.#1“`<£¸CžÎ¯ì((ÄA#»ú‰è9e˜Q/‡øjMDΉö‡‡zÜæp‡m½ïÛÈS˜“ÒWÒ§š(Ãñ…Öm#/£øá®a˜Åøó³_½Ö~¬pýf(9œÕo£9<ÿÛÇá“wgYÜ~ÍõŒà7†êÕ+ŒË&«Ó}w}8×9oä[”þ”b&þÑä\0˜ÀM$—Ç3#/Ú$MÐ#/@ÚQ²ÎZ—ظޗê#/rW—»ÇŸØä{>žÿÖÕ*«žp5Ÿ°äBÏ8Pv½#1TiŽ@æÀïpj:]K¡è#.s.?„zs3*÷'Á˜ÓÔ†Ä=pYùEHÁdþGÐ \4Ü=IÑêóL”$€dA0%¢¯x°lö=.î{;4lŽÿÕÃÔƒªì>ëï\tÞ\¡µbþŠôˤ^Öä+0fXÙ#/à¢EÿvFDý(ò+Ó½Pâ‡6±!uS~HP–r0Glín7ê#1/@qßí¦@#/Qõ†àp6†êÅUE„22CP¯ßíýi¡ó|~P!2?¢³Ø¥âÈ%w¯U%"^ú]×o;v‡‹xÕ(?‰óÍ9M2 _ÖÍYšÖ¥$>NÚËBŽkœšàôÈs².yãƒUÉ4ò(Åé>SêtŠvFm”ÊëŠU¦ÆKµoèµÔ°(›5*æ ˜#XÅѾ\üs-Í2&Ý…‹#1OÍ€`;?p¤ýõ¶G¿ãì÷¯¾é]ö°úïCíÓçüÖ„O`Ð÷ Ä>ät!õ%Â#.eòOD±ó:r9#(#.ÚLdÍ~^?£Äôœ“9‹¿!³7¶Ž¿E"‹Î!PR¼=¸âvŒò<ÉRùoÏ@O #/¤/—è¿#üŽî#ÜmZ²Ô>Â"B!cï;]wwe~žÎ#.F¿¶~ÞLUŒrØòÿY ZÆáúñJöÆæ§EˆbZ ѣú5ˆÛODJG((]Ô¸ª8Yu_‘w1Uc‹ÈRH-ŒÓè9öâWøecÍ\r<éÓÓP”ÐxSÙû0Ñ «ô¥Çé<ó'TŸ½êýÏÐãCT#1z'Lš·#1ké@ZI©#/MîðuÎoÀÓÕ9¼~®|ñ'×(”ë·Ùå=à¢å¹=ær ÛY” øœ8ŠÂÝ„¼êÃo¢úQ܈™Ÿêéµsd1ü‘‡Üøª>EÁïíú´wR»¼ÿ†ÇcðH|#.À‘œŒ;2üÿ ñvN¾ØäˆëújË52¥¯Œ>d×ö½v#/4Ðûüõc8Å…#1¢_ž£FHF˜ÐS*4þTû¶wõU9wøÞd¯xÀÔГ¿ÜQlE„@`DØÁqº.0ã„@ô¨//ZÿLKóÿ4<ÀàbX‹Ïêåã|ÉÌø~wëúïù´õtÎkF$-¡Ö;ß¿…ë€|B¶#/zBB,¶Çt`àø#.ƒ×ƒç8…¡°‹žëï³@ÒÙ…\ÚþšùçîÛØö'3çüéG¡ÆÁ Wä‡Ø£ûuõãìÕ\¹bÿC•v 1ù¤žäb3ßýâç÷¿–„~›š*cD7b#1ÊK(šLR+@ÓV8A¦˜“ZrÊImȨ²Qiú#1¼P“EKd02dbliŠƒ´ÈFà]På¦ãõ0W2ËÄâN8Ë1`E¥Š``E#0ÐÒ)"Òj°ŠiH¡šÒ&Ôÿ/ÖÎCä(nÂ?ö2zHY›2Jñ-FYÀ¿¾{ÞÖƒþ½b(¿÷åaδڰ‹Û1j»uL¡‹k¥Çw›lî£`ok“´äèÅ_¨aÒ ;B16c9KZCjnb5ˆþû€ÌCŒGöV{ài}ÔHÛ1¤Ú|¨N#Š=ÁPüðjnï—Ðõד#/#.H‚ æXHXl^{~„ýÚ@©–§aú5³Ë‹9‘P#1Ÿ×?Î!aÙ_R±2Å&êó~Åb¿[Ræg3éoîxMfùgdþQNÞ&Ùzuìé“)!üêT–‹€1#.ÆGàÙ ~!q<sOKŠs~D‚\ë%ƒî=<@Kü¾ÃZ^Úçzmï1ˆ‰ùÐM0â¨n†.múÚˤÙl(Økls®Ög5‰‘½Ê˜¨„B7üβvÇùSêÇè¼oúIäG¥q9´æÆZA"-$,-E‚‡’C˜ÿÀ¡A¥YýßÖfï•ôÑ,°ýU$,b¶ å‡be! )ßQøð‡“,Ù$%¥«`§ìàã#.º¯wãúM#.ýß«kíéÁCñùÞæhãûj\Xß¾}ï«%"†óÞfÏÑRkÞÿžÊ÷úî?ýZ¨’Ø_~è5ÀC¯&N¿b0c€u°¤Ž|¢ô/ˆp‚}¾¥@ˆâÙ9#.±ÅÃ[Ä·Ôbk7DË%„~‘Ñ@ÚwÏ–=J¦MõNl‘»´pT àwøý×7ô£N´&Þ?xzì|Jν&´iüòM›¬äo„YÆhg¶ŸLœ5:×¼ÛÙÕüòÏ,é¯0‡¢Q5½TÄS1Ã9Ôïš ,áÙÆ>:ƒH1ë‰0ßQ“X’þ'™X7³0èzq¥ÑÙ#1,É<ž¯ƒ:õQFÃd™rƃûJsœä]‹žÏõžx¸ÔÑr±²!îä«Û–JÃ(»(Žà‰Øþv»‡u¤Ê¡šÒÑhôNîM„/,¾¢p‚"À<0G{‡ Q¾/$¼&þ—zN¦ ˜ø¡7³ü†%!¦¡³ùí"/ÕéTCFzñH~‚s€Ïß÷ÛÃîñl.ŸëôÕ_¥Îøy#Tw‰#/´þ÷Öù¿'”_#.hçG‡êU¸v³ xÜ&=Ñðš@¢.`‰šÔL#1seþ¡‹;w/PÒ2‹Ý¨%‰¶Ç*…V^ÀØeÏ!‹µ”‡¬wíÿRPj"b…y7j{Ðÿ~Ÿ¿45‚wØÇ hÕW#1÷»Rø,ÿ™Èñ'7Ëúɸø³ŒèîÂl#1šú xÇ™´GD&>#1(ëèoáôZ9‡OÊü,ñ·5›±í¸t>A:;ÜÁê€1FUO~C¤X8Ùuˆƒ{;ØxÇ#ê.ƒ¨=þ/ʶ¨ÆïIÝ&^~²Èó?–åQ@Îê}/Úèžéý#/üdŽŽkÔHNõwê)Èû¿õr`®l×°^!#.m”*f½âxÃEÔÔ0{}RF’½ê‡1ªçX^l|'-9–:ºõMÕ¹ÿCNù@KE_~i#KC˜">Õ͆D+×µŽÝ€¬1’üïÝcÜËžár!Ö f1!6ÙF@ã•4±rŒê‹¤¹ÊÚv¬ªcåÝQÏ–Þš)1&Ý/ÁWtÎ z“ÅmÙþ¾:˜5ìùd¼çœA&üÊÂnët„&aÐ/s¶pýŒEãw¾Êxg#/tòà¹yÏM²ûªæjúóßNšë‡^¸ÌD7k—£b鵂KüÖÀT®i;Câõ´Þq)|.`il‚ª]Z¨HÅó,›f¶6Å%¢K•Ö°¶*#1d ÀîÌA¥Êi]]ÉvÙby@þRúŽçL¿Lå2Í6eå¨Ðò¸Ûã9”öS»|r”8˜@…RŒM3F‚×Ë«¦Ù©œ«Y䜵宋Ù[&¸’˜UÝœnLÈ¡j¨-õ´#1¬g™ïü•ç[Ha¿>·Åàdu\L#1BéiÕ¤œGGú@@#.P¯ñþVy=Ãê/ý“ô›/GA‘ˆ°h¹²ÝÚHhGƒön^pÂ#/-ªá4væzÁçŽåvª/Ç~]î²+Â?*ï[ô׉¸Ø¯×µYpÇ„jNüN¥¶Ö£…ñR¶ÙËú¯Ç|))ý½³_ƇÁÜ_.“yý—‰Î¸r‡[B£=¦ŒÃÒð¥}ø~Öì—ûÓÞ§\¾'eÂt€~Ñ~UwÚ¦ÝÞW¦Q!™7ä‘…‘ØÂ3³÷@g0úäééÓq}z£å1¼ðk´Ñ‚@¤! ÖÆJ ¸æ?w7ÂŽFœÞ˜íEÝ•ìßËz ŸÆ‚® ³DAFS«s0çUÎÁ¡û÷#.>ß¿Kó'Óö'_|.»ÁQ߇˜]ñ‰soºñ±m ¼Ž[üÜ÷Þˆþ¡76ÞuW¡Œ@ ÙJ¿’=g×H`<rh±…´·U´2»²íÔ4ŸÃ£àïÚ:#)’¬ÛÚgßKâ”àdŸŽ¥‰Ô8!HB›>®Ÿ½9ÿSŽ2tå‘ÝêÚ¢qç+f|lmíв-àÞ\)—Ü¡œ*%W Sh>{2mu>Ê9sKNŠOŒ×›¯µ9MûS:%w¤Æ<~ÉnçG”ìd”ÀšxßÇO<ß#/† ¾MëÜb¦M4ÐXɲ+Þ…ª~l§uôÀôF6c¸÷KÛßíìïÂ[¤ÑNg¦p—pöᤆî€Óæ&ôÙèG'C[Ý}r³ŒO_ÙF›wšÚ馔ô…²§…켊jò “·³>/9wGC‹Zð·w¶Ûý»÷[{ý|CÓX’c@}è®$¨NG#βy¢;#/ *‘a‘’ê²G“˜F(^¼üó‰»RÄ`å©.€—ºæ#/1ɾMDô¤•ºþ¾Ú?©úgBYš;K(¸žÃÐD$dSê`ÿŸÕþèø¾sèü|ô{?‡øVÎAˆF @ëgóî@Ðÿ‹{‡Þ>lÕ”"’#.Øçz¦âQÈOÓE"ƒÆ!Ûƒû7ögïð;-°Øj}–Aˆþrñ“ó|«ÁÆP 2e$OQbä×òø`‹ü|‡Ö‡ðþ—Ìn!ý'’l#/-ëIpÏþL³-몪°ÿÖÄn¦ûÁ÷è0ÿqÅ&î+m#.êÞñ2 Heqê&a·¡ý@víÜ=gph6‡K½¡û‚ŒÛò%¤Ø\4„”‰¡û^iü,½A¿YÄÕØp*UÛÞg¥‰$ÆG|ƒRp;ÌÚAŽ xf›íØínÜx‡ŠMSÁLúC@8ì"ÑÞõgÈ–.,}aÇ}(Z«8:KƒÍÜ=Íï!¬<0z€{Ư2{#/à7#.ó¹ëÒ\ÄÆÊ”hó$†'±D7ªdJhfy…™ÔÊÙŸàlwœ~Ó`/H‰ER±ƒÒ©ZªU ÜuÙ›ÔíA¨o#94‚p[#/˜ÁÖï‡ « òyŸÈ}ßÌ•÷×ó¡NfÃP(e¢<ƒÀï9tŠŒCË¡å2uXéÈ^l㦄ó-'B„ à\Ë»èq¾Vá„û&±›éÒ£Õ1Â#/7¯':îu*„MTâÔŽ³§íËór 0Á‡1†ƒPÖƒbhF# .£Pf–€‹Â<8ò”vŒßTnÂ>ÐÎ!b+Q™aZ–ã™:„ÝÁÀ-‹X¢ˆ©œ´»îY}>¸ÉÙúaª_ $è;#·1J%5»!O¤ô¦áµÀûè,€dúѽ[üþN^ì$)Û§·™¬‹£ï÷u^épOáÃÕɲ#Xåùã¹Ý¾š‹2Ïg^Ð|Ž&#.ñíhà (¨~¡=M=…Œ'$‚1‹ƒ–”ZÉaÿ-Æ…š1V˜Û&IPDƒHŒ`ÃØV¸Óö„›–PùÄ#1 *’Rˆ„?ÒÍÉ‘%åOúßÃíoi-™£F±¬oÞÞ÷X^kÙÈi»£uÕúÒ¨–FËûÍ©ñ#1WÚ‡T#ê„# *Ö¶ìÉæ;ÎðÐoƒýTD‡³yAƒ'µíðk#.’lÕ"oS¿°€ûz‹¯S#1±Xˆ<£ý“³´³Ü˜T’àk'T¹0è_Ä3ºÑÁØè9¢6ÐLXr²8´S„aï_²Ú¹8ï£!è>3gù—“Ã$€Ú=Ìá懙t“Y¸'?8A`ê¯xÍHDì7€šK‡ÒŠq!<-5ÁÉÜM‡³ÈskûH¤‚Õ^b§»´r#¼iccPh$‹ùˆ©¿a£”3aîÄæÄ6åå}öÝîžNÄ7áÛή¯‘'Íd¦`[Œrlní·`¹øœ#Ô™ˆWXÜÈ’ni¹×$¸v¤+‘[ï#°Ô0i‡ë9<‹oÕœ-PÖ–NáËòÞ¤J«ÇnI‚‰¸tqÀæo¬‹°›žÒÈl\ÆdìºÈÔÉêKõ£ÅÊD°¯_u'q5³Ë¿{›:2«ßV®1ûáƒYÒsž%ç¬QÆ'I!¿mûº¯º£±§´Ä[¿,Ý7frQJÔš=Ö·“w~ýÜ ñ‘ÆyqM=5w0²#†ìÂmÞ3¬ÂHÝŽ’ºìÕ·t»ÑÚ9áÛ’ù›¨6œ›^:ÍÎçÍÌã}IÉåbP¦³FÓÒM4—îý½Uîã!ÈÓ~ãÐÍìTP¼ÔRžÌ[w<g–äaó%ìt8†ã<‚`†§“uy9›ØXM¤Öm2v#.Ÿ?öÕ4Vïo¾æÂwpuN¢á,Ï#.{LÒÆ€ÏÁà @W S9$S>ÇnÉn¶—P¢`ÀÁ]Gµc;’hhÃUÔé îðŒù3ÌÚ¶ÚàNåntæûbïvè ÖT¢iAŒÎÊpU¨ÛFѶ˜ñhPžŒ&S‰Ç´"i/0bQ_”ƒ ‡À6›Øk¤#/kM4ªšë[u8,A"–¼ØƒÐ4 ’YvõGP‰‚ž{S°ˆ¦÷cžÈ5гÐ-Κñå¢k,SŽqåiS”{yPuhž‡BÌ]rØþ"àƒ4ºƒÊ«™°=Þó½ëóh&¦ÃkÎ `"q´hÌÎÈN¬ÒkY¼zVc8Îr©R¦!5;uX(ƒ¨liÇi¤(Öa%L„<›¹Ð:kÒÝv‰•Ë•yx¶|—·´Ü™mJ)nîþúœ‘™Y-½òŽÄÁGxkÖæ4º@ø`À¨ðvä]ÈòÝŒ²¸…gZÜ·d(‰‡qAUÑÔxðâ„fV†äTÛ6g#.Ëgï©UÁ§cÜo†JPü>ÜÁJ{$|E^y„DW€âÈ€Õ39Ã*׶w̘ÙWYCÝËС6†J³oif)¡›N\EŒg¼ÔÉžeŽÂ4ZQ±cË×€Õr›0YË¿ºà5,Þvi†Ë÷ʽФ‘P"BA¨£¬PËÖÎø¬$1Ž<*ÍSKoTÝ¥ÞéÈÌb,B.aÍëà1!-àq7Üæ…A’#PT³‹EPªˆäÞ)Ÿ! ¨$ˆrØèÔMt““:Žr‹¦§ôš¥Œ²5¹3¢@î(|##/¯f#17xMQ‘¨—;‘2±’š#/JI'/¥…Z©“©GCÙ4£³”9j»›tr<þ?:̦2Ø«ÌË32eÌÌ̱¶cYVe™’J±Û3Ñó^Í·žíz¡´ƒ» =!Ôð¤Bi°lA°hùK4Þt‰`Ö-?—¦ý¦¬¯6x‚.#.B÷lžqZyå–ä™àÓÓï*ã}®Ži …eQS‡œÁ–WžÕœÔ¨ñNZÝK3¯ƒ¤ñ2(Æa¨lÉ\]Œ£AŽº¶¤˜R&d!«H€–Öœ|àÄëaõzƒlŽ¢3²BœÍDÆœÓj¬¥Ùè¶ýs&®ÎÈø#/@éõö%ÜbÁ`ŒJ¢„—T²Å¢ ˆƒ‰g-&ÞÃêçz!X˸&Ý2(øíÒö¡±‡˜L ÍÄ,٩ٷÜÉÔšã®Ë¿yÏnd|ñc°Ûê|íT*’j:™`wuàôõ†²iÉ!àf#”Ñe„¨jÐ<#/-l¯õ„!D:MûÑQVúæ'-HªtB˜°v#1OËÌéƒþc`v¶ z˜’‡›áe¼zŠ¨b2 Ó‡djžId#/ˆDõqöË™l&‹Tï¾Ê‰g*ø$ƒFÞBÁ1ÛE¤v‘S6§1nùÞâbÌ<rúQÐݵG(tÕ˜wàn˜âpzK&d?Š5m²Þµ#/uó¸pÔèm…ç\ÈÔÅHl"YÕfíY5n—¸õ‹—"&ºÚ¥e–\¹X³°þ~vÆÀûW-?ˆaEäûJc£»cžÈ~!+Ž+ÀлhfÍV5WHž~;Coz.„â#.C©/3$96†³4öhÅ(BÎÉøáž@sïn"‚ðŒ„#1i¦Ãµ.lP;pL„¤æÉí ¶e‹ÊûŠ$êuU&PV{%†é³Pzp8›•àb=Áh††Ò"Уž''#.X‘ñKº\òLÓà3]%†7ô7q‡¢7 N"[]záÝ$1·²/z5ÀjÌiš.‘ý=#1‹ó&”Œè,’£>6¨ê1V0¦Æ§ /xy#.˜2²¬ênj™ïQnä9Ch¥I솅î/b=‡¯Óp:7]¡ÇœMÙ»Rɿ׃ ƒÔîc¡Ü5ÇÒ0¢@⃠½RŽ¨È?p¾ÐƒC3Ò.vÓ´ÚŽL3‡k5²nŠppä6f„Ö£©pH’ÆDrš¬ ’i™…UTnÙbÖV'Yê¼KB%Ç6Â2ן±Ì^Ÿœè½dÍÂ*5Ñ ûWJwxÒ’½À´!ë|ƒÄ•Ì ‰S°!÷dÀìA"&6‰¦ËS(¤Þõï/…ÒmÐ.ò.‡¨æ:ŒVpv˜äò#/©´ø•‰ÒüÊlýžÚÊÖ°_·6m3Æ€ÓÚ÷ÑÛäEÍLJ[W°ˆÐäì~$àpø¯P‹ÑvHö 80-Á‡<çrüĶËDƒÓ}ú½ ‡ú&Ä’|bŠ÷àIŒ¤sÿ^{#M¼7R¾<=û/(´x°HA$aç—€wÃÒèlÍÍ0‚¡Ò7ÜÞ7ÒÄDš¢0Óë”á+ ^7µðë›Åç.4Ì`Açˆcph%»ñŠy&éeY$–-Ãp%A=e5±€M€³ööœd}ÅfËØQÏ@B4‹¾ÐllÙú:/Ö}$o“0›¹”˜I-§Óý”iƒ}16xf‰Á“Q"îqÔ&Q A,t;™åmèh ãFö*P›ß:Ö[Õá™<૆€þäteéj€A/ÙM67øúIL´8`ÊKZû_?†Äÿd,¢fåGZ•U¬ä7ü¹g8—#{ðòñÏj†¡ ´!F…QEô}ÞZØÿê¼Ì̳ó#ì°ZìBãß0gJ—#·SÓHß·rz”D¢%Ží¿Ù9å*Š%%Nè‚ÅHX2 ÐO¼ù|žî‰ëÍÀW^ä#1G“‹˜9éÿAßõCþÏtË0ÿgûg#/KQY-ÜþQ§WMµ–i,¿• Vß]ù¾ÛôZµwëT÷—R%ÌÙ%r‡÷EYG‡‡¯AñÊCw¬.Û?¡‹‘ÖŸ#1m~Àè‡Ú!€1„Bi[Ûº‰5ü#/·Y™JDêxíÈ%P^R‚)j !#D«ù`Â(>Þ•D}¿¸@جõKñ]"Aån¾%íåƒ}[â£Î \ð„„”^á~|´Å%‹*)ùÑþ2E»T#1™@¤-j@[än ÝÖõ§ýì¾tt°ž8I´÷ä¤&ˆkj¨›ð:àÏ)…@82*#˜zտ˪¿9T´4@¹NmÍ àÅRQ)$oVƒ"¶Ð_ÏCä"€ˆž¯ø°`’MêHŸ¡úÜ(4ƒè‹(]5GrEJAÚdèDO}!¼Œ#.À„’CR2H‡»[tÐÀüh,e×¥ôÓ=8Ú¨ŒÛ¯“>>Vã3º±tRaÝ¥`™Ò±1.C@À0Š,D…´nXZkpqA„¤Ê0?î7D[ÃÏÔêžö´šQîÓð³÷‡i58°„¾'.ª/qÌyU¸|n¤ÀÃäÄlFO×DªóøÑ´B ¤=¦‡hK*?m>èIœiü¦f‡ŸA¢Bn¸·¦»Þñ @c'rJþh¥n}úWK¨ÀwçÚ#n}][_ÜŠú¤ˆËæ1ÀªyÄ€±@69x:Nà„A 4"ÈP$ Å „Qöï#…@îˆ#.ž#1‹"†" QAD‘#.!$[ȸ”(7HUŠœM;'à„>=°_÷YÒáq˜*5—Ñ£:_ÕP±4P R|@BÒÕ™¨-‰Ö 0k#/b¦‘ƒIŒÐ0D8BHMë4¡ÿUG’°bûŠ½1x¯E¹u$‘oWvñéUÊõ7/Sx×bhÝÎ×——\#/£,ÏbU‘¨0lŒƒr%Œ0L$Cy`ȈuGƒ#/¸eb’A$™•ÙŽÄæc¤Ðî&lƒ@› ×m·é×Ùömµ}ý^ÀCìdÒoô5%€jY|Ï ÄET$ŒäS´7•äö{_Õƒ«ç+tµ¬Z+¦Z´cPq–BŽ>¿ä{:÷+ò¡ì?½¿‚V›‹Ž(Áf>¾gŽnÍù“]2¶ã¹ùú)ôõ×—5è/ºøž±Ï ™ƒõ°é$N$¬±î÷–¾åÂlÝþÊ{Ž-â¦lQSdJŒ DVD—^¤l¶½V#"ü¼oÕÂÉ"?µzkaÖÇøÄè=ß>6 oD÷#/TGúËPmªoÔÛ#/i‰ì¯à`Ÿ¾á,çÙ}\fɇ©¦²©$ΤB˜ó®‘%̆MuÜ>)i2E@b†4¶†›èGƒsBÒ°¶KÀºasýLD2!—t#/£¸xriõ¦ípð7‡?å${ÖþË-ýßjç·Jzj~….SxiN\=~£u»â~_8Ÿ0XmÜðÝ#.>꺫™Æ‰ 'PÁä>]-RYêïmaúƒWEO‘$Aõè{²w÷Ó$XðõçQ&'§˜!<ĹuU4ÁɃòû?É'D’³t},ÃÏ#/GŸšç¦K%Y!·ðƒQš6ÎËöîø›c–P[3ÐçwL;º> ÒÖæ@Óõ÷‚QB5MSECÓÏóÞ Øy´QA(í6§²“e/o9érM„\›UúªŠÛ“fÕ0-^T®ÕWì“[é¬ÞgÊئVÓKëæ2G)%}'\l\ª’UIQ)†ò¶ÓÄÝz¨[Ý—¢˜…±\Ò³þY*#lMƒ?%ü+<·‰f3$}¢h« ë03ˆ>:qËO£âwÆJª”Ò ‡Neç»Îi;v5—ô{×®¶¢üh* Žå¥*+ù¡pøà !"McmFª’µdƒf¬i&Ê$ÔRmËM¶±4ªKM¬V™ªÌiQIŒýLhT"Æ 'C’IÊ}3ªß\©9ÃŽàSЇ ŠDõA¨ƒ"ŒŠ½-oXAòB”¡T>”òw¦¾Fç½kËdFìe¢#/s`M¢~Ùs!Cᡱ×G5j¢Ñ£UºXÌðYdRæ耘Vj„í›c8~ÙzñhA7¤/¾\BI„"p1†ªÄrųDÛÄ1°Sn•ò¿Ã‡ÈÈ¥m6¡‘0°AWÖ¬´.Ã#.KH¹-#.]s·‚ÝAx lAm`ÚÅUVùþµÛmy+D.äW«Õš†{˜p—T¬ZŠ"ÎÖ`·\S&eÐ 2b%#2¡bbS_öãx|1úˆö0 zúè#/2‰¶TŒPª”à€íáʘ2€¨@!ŸßƒÛæÓ_}b*3Ccešß{5sX&ÑXÖ®mËnVæ™P¥Šf‹f`ÔT›h M"‚Ágß™ùO&°h!Ù²v¥æi™±Cx¡"#.DŠÈŠ9*'ŠŠD9—דêÞ?…ï¿Ú~?>-ÕFËgFç i#/Ü\"oúU\È'´æAâW¨ñ½O°*† 'ÍP úHFãiôH|Íáù¿7Ýüÿ‡Ÿü?ã´Û³R¡è‰#èœr¢Uô± #.,îô³Á/#1„bª*¤0ÀÝ¡$–©'®µ '蛀 *1… mt\PÂzURj}§+ÃŽg™ááÝf’n!±Ñ¥ÀM³þh¨ºçK”ÔOŸ€2ûò×2¢*}Œþ~ÙXš´È–ŠýP7N¬4—ŒÞ^N*îŽQŒÔ/þuC»½ôèP:åP'ì¡tDш˜i¥¨£ŸqÔ0 ¤œ±á¿0O#Á:6ì!Naúå J©ÀÛÞÿ6Oiھ͡±Q›Áï–n+Ð<Ì€†þãm#¶l–A¸Xà4BŠ(ba5K¶0œ:=iØ|ݧO²Žð"»wæIavÀx‚ÿ² C Îiý#/gÍ׿Kî»öéA”6.PC;0kèv=³#¼><_Í™èxdƒ³cƒ“ï¡ãlQô]°öáâvr‰Sº^vÍu®Z¾_,¦IUBMRmŸ©°"dJ,„Çä£f±Ìü]cÕ¨y莳HiŸ/ÒÒéó£ÆÊ$#/9—#/MÛ~ïIz1—?ô\Ü0ǹEÿi]½§`DLòê7l\N$[—án#1M3EˆYƒSª¢nˆUÌ´®I¿$%.!Ì}ÃÂŒ2ý÷‹%³ƒÎbo;xª$ÙÕÃ`Þ`ÓU5s‡-d¥*œkú³{Úãž7.F‰Äh0dE +UzPTCšbÚšíXY5°ÚÂ#/²Ë+IÜ!é ˆ„`Œ#1>æ“e©P÷¤=4y‚&Óyìò~u¨8nGè¥-#.éÈê¬Åwþÿê¢$!gúÏìñöW¿Jêø7:î»N'd£O̼¿O޾ʾ¯¦Š“cñÕÝÚîR‹Ö¢ H½~ÏØÑ"©˜#”V"'·4 ÙØžžß<¹ÃåˆÞÙÛô©/Å)ìÇ÷¸F›’CR{:XÁÃÎDwL'õ)[ŽïÓ#/¥$–˜€€º«VqêöIûœµ£]åƧíÔî` ŽFÇ°àÑB€~¥¤îXA4ŽdH@×h8ü,1 M*˜iè]Å—½qÌæ 0ßλ<b 6E»ô_ñÜ¡öÅá™CÏ‚À‹ñËN…²å}Á—7iÏÐgT´Þ.RäÃWÈáÂ:y“ê-t‹@ ˜š…Ðý¿!di¡ƒ¢`²lÆM#1qf®ÉÊg]ž=Ž¢I>æ:çö4ã“äqÑ0ÜÁâµÑ’¼_i‚YvÀÛvV³a;m6(²Ë#„à#p€¬L™VÍCn«0ªSa\Ó9Û‡Û\tŽBwÚy¬ª:mñf‡£¢åF#/†Ãté}E×·<{ñ•Ønyždƒ–‹$äA-éPŒœ˜½Î¶uäÖv Ñ«‡ Îé°W³¸)Ù±ƒwé‹cSnùö‹Sµ™ PV;VìÝ3[û5îôgw›¥yÚ|µ_NG‘Dþi(í:Á¿‰ˆKã˳Suº#`¾V!G©£ãñÄæïMçÛŽ#.»d<&hìÞ”K&66.C{ðo×îæ}ù©Ãŧ9¿²Š! Ï«[%ãÛß< !ǨtŽ½$*{Œ/#.„¾”F}"(ReÕõ^/´?<åßÝ£ìm+g½¹¦³àW®}d1àiŽßs¾ý`w‰â~õéÓ_lM´…ZpJ$"¯F;¸Ï8`Ŷ~˜!ùÁS–‰Êù4è»ADDšTnÒJ-gjŠ${°³•ë›b§i•~p‹8zºƒÂ2§ˆÅ]]¨j¹6Ÿ7²³*€7k¡©Á³Êž÷ŽùÇNV±âï3Üa0påsR@Û¸&½Œíh]É®«¡Ëo*öq@7<;7ë°¨\Õ:s×q´À˜ÝÎ{(9lsÜä#¡ì8’ìáFŽ:î*©¦Ä@„L..·ßÃ\†»†æ´R)õÝx¬ˆÁìÓEd²©º¡=õŒO‡Ãs‡±(õá½Éœˆ ‚`˜÷“0#/ƒÒ¬¯œuy $:EÚ$’k¯¤lAbCw._Ç3¤';Cîâg®ÜÍØ>æ%Ø`»#1<ç§ÕáIý®Þ‹!"B3Iøô;·HüO†ÚG&jkë$’/ƒ;±H³Ü9D9KuHu©£y©©GîòödG™®¤‰8¡¾Ý/„•?R:q1ê+ ]™±Ûl²w¢™±`M¦Ó“ ŸÛ·p„FAmíéªóÝP ¾aÒ=ù5Ù¹@ç¡5N •¨'XëG¹¡…YÂáeÕHƒ#.S,Ù+].l‰êê]-Ü‹’Æ«)mvû<ñãšvµI‚ƒ#6ÐWFBÅY!UHQ‚-%4$]ÆíÍ܈Ž!PJ€Nä—}•@ŒCI0C#.èÝGX&#/É"D"2c$b! ¢HàÜpßÏòcµƒ ù`s†C´5™ôbôù‚—å¹Ã}üýÙãÚ~7í_‰7[„§âûn¼R*`IrºX6º]¶ûnº¾ÕuÝß«ç6A;ì(—#.¨(œÃÆ@<Uó[º/)úÊ£è§àö¦nõuFÔæ ± ©Ñ‰Ž4:(,”À1g}`ê^óC&¿©!”ÎêµÃÙaÍl¦1ÁPÖl‰ƒmF®àùìb¡Ã Ão‚Š{ÿdë$”Û»#.¯€«M#1nÚ‡ª:@g#/¦¡2@û´¬°* ¤7#Q-ʳ{ÀªÇGqïÓM[Fá<P‚sB}X:¯WG×2¤#%og†xnëFµÍuÖíunÖº%J[P¡HÓ¶·hF‚¤ÐŠX€@F4¤E,)ãYPá$´C!ÈøPÇct½úTç¬75®¾5êú¥5Ú‹s¦Ú£ZRâA2‹ 8fü›Ð5Iy*Ÿ~M}ÜÙ=s€¹wqåGàf·„ 4 ³äÓû¾fÄE56Æç]7€dÁå†àt‰ð·nÉ#.M4Pßrt‰®nóDT±¶×3jªªv=A;H¤‹ v8…pØI}ñíþ5‚b»%ònä쌶Ÿ1Ž¹[U°E€í*l0D´#1`x#1‚AÜÆ›æ×lðhd5ÂÕè oÁ q¡µØ͹‚–ˆŠzÛÙQ "´gÉ UFkûywZÓzw”@¦†Ì*EŠ£œxµêL¡˜*ñ#.]œ\ RlȽ‚ÈSéÆ.èpd4MìX‹H¡·m#m u=šõƒM¾_¤±ÛåU’5æÍ•N·ëؤ‡¢QÌ¢pÌã· ‡ÃŒÔæÉó,8›ÑuEèù•tº[Òªuq5'eŽ0Þãí½íxN…¦ì)j¡Hδ²—<NžÏ©¶Þ@ûùœ»—8G¥ üCóóÙ&·Ù”hŽÐ"2ƒð^7ÖMHñŸ—[DvH†³füÙšdr5ÈeÎ>T {É$‘}AëíqöÍY‘#rì‚[†‚`lsŠÝÇqÄPXW4ðÎPã(€ºf”E# “° к˜dG(pD#/ÂY¨ŒƒÐ€¡¯5iЗ9xT Y"q‹›Ý•ì>añ¢ ³<Ô ‰®±#üÆÁ#1=€+ EסY¨ƒ¨<X-‰ @‡’„©¸š`œm¤ˆs`2F0S#.CÍÀØÓDÃB„¥sÔ‹Z""ÜKÃ.BqÂn]ÙPÜ0ª„¾ÄƒÆÑŒói±#Ð`” 6Æ1°"wœµ¥s…X#/,#1KD2–çVß5ÕÛo=uªõeŒ2 u)a‡}Öì‚/ÛÎ\áçœvþ>!l?gÒ°üô„¶KËRöER§35F” áš¡™pãX¹+ËýÅ'ÉÚ(ucíÄ)(‘›¯@%A-¥y>ÃH®–0›#.šüv»ðdBn‹ÒÖcµøÆ@óè¹ $Á«o†î£h{Ûõ_BK%ýi¡Çɘ¼û8ês?¤:#.³Á';8\KX6Uv)ǸüqohÀÙM-ÍŸ´žÑ}LŒ>†V–´»·»,-,tÔ ›Ó–^ÿ$}Â侎ŽÓa¹ë?4ìté=µåwa"9u6%HrܱýlöDë'oáB&I±EÄRª$ƒ&ï‘ùR#"²,ˆÆl^„Hv#1 (j;¹¶U.wSZ‚)tÇ–Ïf2V.GÉØ߸ðõ7âþ™¹Rª«Ù,‰Í¥Å)qY’ Ï€):Íôû^OEíåLs×Spïð籩TÁ¦”"!M-¢×ÔÞëã7•ã¹Û¹ÉŸ¹‘(”¶‹n¥1¡˜B7ùFf°,×Be"³&"Š(‘„”¥#1’讓0“2söXYâRŠÉI"‰?àÇD½Æ}ÖÆ”ÌSY"1bD¬èçÐêº BeÑ«RVuk@õ¦ëô´÷+a ,»›Ð&jƒ¼Y~Î Áõ¤°ŒU¤gD„mâ1õ£"yÚcý1î!¦%¼šŠÒ=0ÑPŒVYZXMŠ<)6BO33#.Jôê•U7M#/ °Œ?RŒ“¯98ͬ¢µ³RVFV© <Ì{åŒcWõD(ÑˆÄΆ>ÈS†Òn±é_dp6¶k(ã׌’5‹>|7† Œy³h¦6*äIT)GB£ÝÊz SY2h#/P’a'±ç]ߺƒç³æ‰wawBóx¼ºÖ¡ØèbXÀá¬ÀýÙSáßG¤™´&LظD„“[]…±E4iÈîÄÈÅ%k«Iˆaɨ`ÊÚ)!5¬t?/––ÇÖ¸ª3†Úâ½Bzµ¶LÍaJ8&½{š‚P÷/6[$ ;P@[kK#w]iìîóµêiR5²Äˆ¨1D@@FQo6ÏʲQÙte<˜pÖ4 ³otˆ+“e8¢Æ$ŽG‹•B†Ó؆ª Q©¢<·ËZïmŽCÖâ}œ2>/xy%W«]Û¡D¦ñ#1¢©ÕKÀÓëÅ Xh×ùÏí9¯½¸ßR@jëÚâ•*$¸òäxåÒ#1‹‰;_Db¯l˜Öû^Ž0»Ã;#Ä„©ÝøxF3cjŒuMþt6C¡7Iñ¡”CKV¬7ŠZ)´ .P7‘CQd TS„™$ !‡aÙ8jùÚSS}2ä®ÑÉj+ÁE€ƒ`7™YÄÈØ[»µ¤I¶“(&¤G(™‡"圞HtÅMö§ 3lÓQÁ‘©·VÆ£&¢S#/Øh’’MFCd›6ÍËL²Ýœ(ÉÁ‹LÙJ1åö¢pNSY’´›„""lˆ|,j°ÐÂ4•3IU-#/ÐÑàÀÑ £"#1€°ÐݨY«d¯òˆFÿ'<·Ä3¹ñ‹ç«©RfyR‡]5ˆ),¹¾¢ÍF0Å#¹‹Ýê਑Ñ4){GÅg©ˆ6ã<œÛXnÄiÀÚÿrN ä+Øpy¿#oÓ!_“.:`”!°››#1@ƒRçƒOÖ¨fݱ#/œá®”¸YJb7 œ¾[·tÞÉcyTM ™r¹A&-DhˆtŠ:‘ wùÐ!‘»È°X‹Ëy¸,ŽØQ/ÄÖÿkàC1â\#/¢A„Þf[9½ÝŽT‡W¿à¿ŽÆÑ3ˆIÒl'µÞžRdMr§6oYÙEØ”Œzœö6vÁðçŧ/ÁAD„Z2&\Bf I¹¶°PîUFÙ„°>*ªÌM¿M›ê&)·1¯Ê[qrÁlútI÷F\æá®2õ'5£\Üh=›ò}ö¥‡4·®b[îše¶M€ëðlÉÝÊU(ÑÍí“Ø| f„ÇYìÝàs`Ç!ì›ú‘è*œØ†:~®‡Lù—ˆ=¼†v ,œP Xï6ü}„âžÃiâY‡hm)‚ÄX#16`—c‘#1“!’ì…âØU··#.ÃzºX±X[ÜÝP ŽmÍ[VZ"HX& ¹1£¬’G1$j˜"%Ž;,˜™”µ¾«c ÒÂg—4Ølë€Å¬Dc†K)ŠQÀ7,ûÄá#1:°L#1¹Åy¹iö‚ðËh[LÍñè üÛß™|*$^£ù¾ö©š¼…Ý,ú{ñÛžEýƒn”Î#/FuÀM ™µmÏukãYoL᪶G²?‚76MÚ—ÂheðkµXѨξ®nŽeñ<Õ‹Åù\FZ)™—¿cAži7ùYVÃMG†~ù¢šU6ÔÊîµtÇ›±ùûâñ`0#.’`ô|¬NŒ~5<HCò`ˆˆ*B©M¤”XQ¨Û^5\U¶´j¿‘ª¯ÛÆÛZ #.e$(Jqõ}BðôÛÀžº0'žéY¡—ã¥JO5ÁÙ|=Ð[YvPõÀ'¬÷u¡°è†fGÈD{ã1“ˆ%)2S1š¥,Ì•”¦˜¤Ú¤ËIûn¤¦´„ Š‰¥L›4DÀ¢Úh×ÑÛ˜d©1d”JR¶hS&&i™)Ti‰¥KåÝ@FÅ‘¡ALi%%– Ìš5TÔQF¦FÆ2kJ6U!)‚RTe6¥Fb4ÊÀ¢ˆÆDQY¡£Ì;Òk¿mNÑ;€¯~•%þŒÄ30:I°VùÀ&Aò~V6˜®¥Ú%ðq®Ã‹ñýûìî”'ôJ}c8C8£µG)V¶~ó$¾3š ]¼·. šæqu¹wbI›÷òjbÅ#/Þ’Ãûò†#.r1¾§>ÃPîàÞ¶Š$F~j=ŒKñ7nn1pð´ßD¹cpäæag-Šm[iÙIvmLñ³(%3ß?¼3ƒ=m%OOÖMÓ1ëÛ#/Zb¤þr¦~_¤¶‹¾üBñ†›ª YCJÞVÈàd\ ‹¤·ì~«V÷½í¯5õ¶$cM´F¦²m‹DTÍfYˆJaå·>\k¹Ð;¸qE¼ª—0¢"¹N‰ÙÛógg³>â=•Sy¹Æzõaäaк´ÞÒ`ØÃBî5Ùá»3/¶¸æß9{È«ÖôúD€÷´Ê‘®çœ÷”P#,&¢»öŸ~\¤ý0OÝîvˆ…EĪ!ÝTXWÎ-™]Á“`Òf[7õüÖl"¦ôÓDéѹb„Æ\]ÙC}E飇mUYU„íj‰D@ ؉¦Ãä–Ц{‘Þ]g#.HØdQÇYñûdË8ö0Y>ý.€½žå#1Œ¼~¾ª«Ï‚"b8(ܶ⟋wåÞz‹gV4½gÆÛfºë)kwœþ)ˆ†ØÈd]&L]/ŒZkZ5 ñ˜Qå™m6íN/Üö3ÛƈeÕC]”ÖúõVÛÕßQ3Å»oÏ*ò`é›KÚÉ´/7vܱÆvßa:lr†$m£ƒ×@#1êãtöÐ@ÅÐI´±Ùš,\ÛÎu·×äüÎ#/ŸvÃœµ¡Ü2…óàSËê;xì¾Srß±ñ‰Þ!õ³‡¥1Fàv-ÍšxÞi[šóK«@Û&Öƒ\“XM¶ðšÃ¶)9tûžuóF•jàv(¨y <6îY¶CèÎ3ëÁ_ê{ÐæùÝ´yš< B!MáÞ4ÏRyÈ4»¦°35œøªM‘:Èt—Ôe 7NU#a%cKG90L¢øtˆ‰¹'Èî@ñê™ä*~¿tÖÊòtÛ¬öoãäÙçÌ™¶’(ç¿è'iVúö;øW‡À<|¬ý0°¶zƒ×õ~šìÆ?¤ïµð‚R¼Ø´å€³b¤™”œ$i‚‰YÈåˆDÅ×y¥õЗÈÞe~Í´’'Ô2´ù€[s”Ü”3¥ œµ)º‹(¯»ÁõÁ&t~ö™kêMkw¨©.ÝÞ‹ûbì[ÝHÔú2¤´n‡L¡Ð· cqÆëØ]FêúzÔŸ^Úªj¨äF_ëâœîdÁzISW†Ã(K:iªfDØiš“ÉÄáƒÊz½„ôT4†WPœ¾:tEt;?€@dD†¨,!ØWY!S”•ÔïÜÙna•òvß0ÔI éUwkœáÄ’ÀpÕDÓ 'ØÑŒQãZ‘Ž7•—3.ÍF6Ìš¨ƒÔ `ò¸Q‘q§F÷5˜#/§íÆf-åsî˜cmÍFéÌÇÌÓjþMd’6mΡY#1Άaeí1Qûà¶òÁ{f2`Ãå:“ æTCKÓ\ëX‰ÅZÈ´]o2fFsû²MáM««&¡yÚÛçN×x&ÞO¹„ZRÞ'ìÍhÂ\æóE#/z§’u…øàSt¸Xðꆨ“Xqô×½Ò'$LÍDiS‹IíTbæe –•Df]¨²”!‰M).IqF\F¢%‰ÚšÖÈ©ÄXÜ|éÇúÝ'Hy{—µ&¢0´d»ÍoyÚö²²o¹,5¥’F:glI;uˆë eó¡ûêúë“ŸNw•Ó‚DòänÛÑÕÝ„„..Ì)9w”@˜©~Ìei¸†%_WhmŸ\Í„ ÒGÛXÿ8t‡£v°DnuáÚësuF‘ÆM §—‡æwH#/³:jî•c«r xå` pL LšêñTÒ;#sc{hÄÉÛ²D:Ðú†œ•,wÞnÖ2ù߬°¼>7'9ÎÜÕW1µ£ZTÊÓôÌeØÒfiO–›Š,ZÝËÆ —KG&üfZñRÿ›‡•Ê# v–xˆöæšišºÈS‹†»>M”§RŒ`Ì᪖(ƒ%F› 7üœçÝAéþÁ¿dbõÆ x‹@ìIÑEç*4˦öaè‰Z»ÆøÖ+y3cs7ÕõSÆûÙ¸©™ÝÒgóB‡C¶ó»¬S‘q\GØ.5€ç1Ç>Ùá^u¼Hºt17Ѷâc“Ñ͸(„ýñ’tŸ“9šÞ#/Tç|o ùÆØqÁ$ˆïÓkÕo|×'fÚѼ1)šûÝTµðž\r+|¾5Îu-ÑçNB÷=BT;g$éö¶ææs°Ù;Û½ª0PŸ1ʬîæû-\5ÃÃUäwN~&ƙّÖóa#1Áæ7#/x2f£å®k1˜,}N¨xÊA™†¶Œ…¾Í/aAc˜CÐc;´ ÎÓ0ÀæÅ6ª@‰¡Æĵ®2LGÍ–ˆrÒ낃*~âĊ¦¡:f?j¦²Mi# 'PÕÓø¼ªr`iYwoœHVÚ¸Š²\Ëc˜˜•³¹{CjbM ™2̬5’66#1…ñ‚dî¸Ô<@?&D[©.Úiz»š¦ …ª1‘å*eÌÖìK ™€á#/Lßdµ&ºC¡æ‰)7lõƒTÉ“sÄDõU³Éc¤[$Ñ&¨x0Ü"Îâ,Á/´l·LRà߬¶Z:qNKC’šõÎîÎrÉšœ@âA–ƪcwhCF„—C¦„TâÓSéJ~°Î ¹¸„IZ"Ñ iš×mšÔMÚüÜÞ¸Ò2§ÚÝñáÓ9ïi³½nó‚/6vƒÐ^¤Æ^¦y0äË+]µWCås§£Ê±jÞH§¸iëV_.l…ºbJ©ˆ½¡PtXô¸ºwгC5ä+Þ&SeLŠ®xjh›¯È…ñ6ô>±¹×Ky_6 F„=[9[l;cm‹Ä.!\2±‡WE°a(Á!óYšw¿º¨¼a]#1 Œ\D&“.œüù.2Ýô™tjFÛpó Ü#5&©_éãFäÙò¾'sdÌ}ÁÃ|GÃXܳ[#/c®#/rœâ×IöjãºÊKlrè¹I<ì¡4È$b‰É}è·u¼“êLMQ3(dÌ’™ÞžV©ñ‡„„AÔ ®ž§EI#1×I!ž\À˜sÍ5›-›y%%O6;hÐÆãça7dt3zݳ4æÌÊÍVðêÙŒÆ_[JÍÍQ»Ï5#1j°Rðÿ,ñ¼iL(s™~¤Ñnt¨™¨ºª§šUcŠ<Y~8zØÄFÉš}‰µ¾_ƒRáÆøÆd[áãJÛ’#/¦7ep6üÞ5’I…tmš²jâ2Vqñ¸7.µ3Üf´ðdÄÉÁÚ4qqÓSu`ñ¨Ç$|8-<8— 5A°¬Mé–k33S£I&ó[sn³2nØᩘ̺uê3+c³TÁ‰ä˜ì”Nš%Í:k¶³–FÙs–"ºöû)®Ó†&ÄaúCÀn‡ÄC |&Þ‰ËNÉ¡—å ÙmdíÞ sî†]¤Þ0gh8;YJjç=Jb™ìrÓŠÖ+z2ÕŠ Î)Ì iÇ'…caá‹œYL89NœðqjâÎõ`þãœ(ÑÀÌ#1œÂ©tï¥î`r&*3`ÈÑB(±‰)aXÍm0ŠDÊÀ8ãDÄ=|›¸`î{c3R¿¥VˆM#Ëâ{ VŒ’ Wa£¨tزÕTÓnhLp&%#/8x!Ë÷¡¼6hà«[On0«¶{ñ#153b¹áŒY°‹P‹!¼\=–h+²•$Ï&Ö£²6&íÆzõ\Ëq«¨¡H-¤¤'U)%jÅᙽO/œàAÄë–Í0B-¹ÑìÆå¬,2ÆÇ$0÷¥WbìÒzÁäâ6v€#‚ˆe„.ÜÊ)‚9$ȧe‹—ãq³’ÈŒ@„!»˜BsŽphŒ‡aõ™•]S§öVw‡U{÷(<x"ˆ‹Æ©²/"ÛK¢¶áIP$V›¤ÓJ”Ó2É–e†,L桲V<sˆZMÀÙ^•NÉ2†¢˜SÁ–š´Žê#/»8KdÑ6([¤õ„Kc3˜…ZQ®Ò6&öÍz͵¬¨P±ÐR¸aoF’"†-„Ý5tÍM™)ÂÅAãîÎI-"Ú´D×*vC9¶9Á8»ã¦ÚvÂð\ãlT¦™4@ËF7 \«ƒ7`.V`=6Žˆç`¿.”kgzÄc—éÂ#¤šSnÔ¥(æ…/.Ù9–N&Ô²qADg1º.»ãõ"e'Q$teo=9\Ë—‡®ºE±„Æ–ÌM§B7“P>·#yụӇ©Ý¯$ÆÐ8èNr‡‡ÝwºÚX£”,ÛpP´3qµ^q³1(ÝÌs;-ÐæçäÙHBÍ‘œÑŒâÔÍÄ$-5-³VL»ËF6¥‚`—¶VÒläq€ÍdÃeø\uÚäÓXð†à †ªƒ‹ÊhClíBl·lb7µsšËÊbdÑÙHäLøà#ÓUû²`ã13— ÄIÛ+LE@Ê{W{á=`èr^L2bxN— ˇ*‡g€0ÜìÓJi ¡Êàô,4®^F!7x[Ù6B˜E¥ ¡*Ð4—b£ƒŒV$krï#1”ä Y1ìw¨Îñ©˜ºÃSaO Tlïä2e‘Œmµ$`1³„¼x¼~s†\¶:ñÖ5Œí6ëÉÄÿZ¤c<@‹Âݨhö:ÐV0`¹öÀ¢<lTVµ´ë¥€°5:ƒÔ2lcšÁ è~f*¡˜ò7Rár–Á@!¥¾úyØsˆ*§-w‘C!D8šŒVdÌ&5cp²iLöèÔ±•À*”DAÎQ`jÄHÌÊÊÑdœ¨ã9š…‘ߺò*8#ŽYÑ/†4x#„ʪlJ“¨Ð )¢„#/ÌBÀÑØèp£;ñêÎ((¤É.fZCHÝÒÒeÌ1|c±6íJ܉°‹#.\´à]0œeØPìÎ.È6f{ñÓXä„ꃖSS#.æ9™r“3#$) ѹ˜ê`ܹCQÑ0mÈL‹l90ºlJGDBvÊG-lq„¸0Åš¼Žð0Í»RI„¶ZÙÁÜØi2¥¤‘ѧ…#1’«˜\7'Æ©: ¥P@"„#Ée´ÌÚÕýI´›ª·÷¿MÚÿ&H¤"¢m‚&Àx³;À(x&¥å= H£¯ñ5:~¾!ðæáQmFù«íûv˵ÀÑV7w3»’B#.‘ATp>‘þc Ì`“˜*°ÄàÌtn‘OæGœm„Òj$º…àTXªÆDaœd#.È#.Ê»‚“#1<à´øe¼Áùkæ4°lÙë[ã¯G•Õ]n<hœ¨NÒ¿{_+’G¦p~©Ým©:„ï;=̼8¶™ÍÁ¬ÊS5£ŒdÝûi‘iJ°ækO¶–ÜR¬7W£ž[ÂŽsiÁ³f°Áð2vªß®‘ñx3™‡’MÄÛ7©÷ƒ“× DÈC!ÌFÐÞÓ†‘–xÜrð8™§#1NYe¬òc!#/Øw~¾@„uë€Sîx"}ãt#1XFVL^Ç›/S# „I ŽðxŸóò¸n“vK@JÙ·IÓ‹ÄòçuÇwžW.ÉÈ1‚£—é#1#1"–€ Pª\ÑP(l¬‹!œ|à›l7T ‹|WÓšñ3¤2ìê|ø¤LÄÓ0Þ¾”lcí+PZUýÄÙï3pH0¼3¬Ûáï÷ìò@sCÄZ;`&:|Ù/j¯È *ˆï›p1$€‡²A~ê…wwvݘÖë»2æë¦íA«š"õ%ÀK?lªˆ°&ÈR¬àxúÐzˆ¼»G™,ª”?Èu¯úˆï”Çr‚ñ#.xq‘úpˆ!ÍgÌ䇟>€|/8áÛC™#.«R&FpE>Â?j»Ê4Ð}cV6—)2.þ'÷ˆÐw9?[eCߣ¿ü§Þ{.<’à¦Á/º|8mÚ£5ÓVŠôT¢*LCŒŒŒ±ALvƒi´‘Äú#/̸·Ê÷£_ͳe"iÍ&;#.‘¡M1(·¨ø!Šá°QèÕj,¨Ñ„æ#ˆ„¸0ZÖ؈€`pýÏ–CœXv…"%CQ¥{ÈÄ3@ýäGPùWçùÂÅ›•ÀzbbI‚Úâ|Ï•}õ$‚¢ Œ$RE˜´khµ“ŒJFi™hÔر¯øÕ®ˆPhZ€@ò¨Ÿ2ðö˜òÙ[{Àñ ‚–íSvëöx&A’ñ.Ž§|6›;Ä=k<JeJ4jââèDLW…ÄDvôµWUÓQ®W;+_7ou– ÇM½+ ÍÉ<ÝL•¤ñ(#bÍ4¾–i‘H{P±Ÿ¨ý\‹Š<!’GÂßL™qÒt" 1“Û=(µÕÚOê»ÌÀ„{¦áÝÕŠ~ÛQhˆü°hYo#ö!ã®àÆIØP()Èé¶ÁiI‚îÚÈò"5Åi]ÂGGØÀSÞqQ-0Û$.ªRG5´˜@…0”$›²`L00¨¤!-’^…afR–@lÓH#.Üb.š!ä`Ø´ÀdsàfR&lcƒ8®åB+ç7Æ#àv¥£>Ò!¦!¦&4›m´H•Dm€Á€U~Ú[ãÆcEÓ¬¹2¥ß~ó'kŽj„#/QŒU›Ý4]XÊD—T`ëÇ~Л¨2”2†HTŽR—#ÖˆVº¤m#°Ö2†™}„ÖÈÑ×Bç3€`ÙW˜‚i\Ê^K:n0Ñq› º8¨cZšêÌ8äa#.XMN,,šœV;釿7§ŸãéОpï#AMÊ.É ¬„!f‚R#1¦ @ÄE€°Tþ#.é¼KF Š~RH!k#.¥›4!xüö]Ñ#.3UA‚@CCšË1Ë?MUª³¾^–Ó}a˜þ½w®$mc¥¿³1¢´4iŸ³q#18ƒ[éÕÈÂ-Íìà¢20™vg]²gÞRšr@˜¤ÅÂv®ïØ›#/°¢;çÚÀ¥ÝDY£Q$ÙÓ‰u Ø4i†>¸à9hØ-ÙñÂÒÒ¶³QAö¯åîgŒ‡=pD«Ô*&vÙ¤@…/…‹Ÿc0ø˜J#/LÉ#1±!ÆŒt+ØÛ«c{”ÆSX à>…&º'.ç+#.¶ÄC§ß|A¥±o„AQý:N¦ÕÖÈÎRLr;v:Q0îÎsœ>L8f[ A¦s8ÂÂD!éXšëM/±ŽžÍ@Úò•öÂ%H¦SR.Œ…mC§5——já7ši¾¯šß7R8ÁcçÎK»Ö=‰©ÖóH³Œ³µoLhBÙˆc6#/7,ôCC!¥P¤ÓA6Ö,IÚÃ0ÐH`#/K-#Èn„##1L‘fÈ K’ÉÁ LXI~’0,H*„AK'\‡(Œ#/A7#.m͹£¨¦ŒØH¹Ký#.#1"#¼ˆñÛ¼,=añŸS_Y +ú³&~UršqH¢ÄöP0a¨þõn”Lü£¾P*ÃìóñIú¡ðTAšáŽÖ¾Íc}¨tþ¡ÀoŠ‰ˆQm¼˜ÍuÚ˜šIDd=‘辶¤_U •ôîînçf›ïÖ¼küõ´ÔlmE¬›FbÛ`µImµ~q$BD„‡òS4>¥ÏVËÉR±5•ª ÏÉÌË·Þ²Ÿi´ö'T+½[;Çטš¨ß=,¥¥÷o•ýÀÝ&°VâÚÊÄš¸Ž¡‡*ÒJäPÙ7¾ºpÖw‰FÊg¡ia^e…8òíÖ8ûs7Î×fŽXºa¥:ËF¥¯e(Åt¥444“lÃMˆ…´]²«ÄÓ†EÃÅHT·„–3y&ÆŒÊdz˜œ9ݱîÈ5Y&®++oz¾HÞV)§"$Ï3Ã6d/l"qS&_[":úkz$LãÄç›h4G·F-³¼L³Ê[”¡žƒ¨ÊV³Mf2±5\øë6pîDuá–ž<aãNCÀæù,+•ÃÓrèÛ^NAxg•Bª“yº¶j‡#.:ÖÝ%³±Q#/³šÚ¡êi÷¹‘Nî:”,’г'Xt+|:£,ÎϨ̬Ö'#1v¹$›Ùºå¢pýêÞ6E¦I5j+¥n³ƒµòLe .ÐÚ²I²Ç’AÜP“åP÷CíLŒ6MLŒ‡7¡—q§Å£l¸`4LðÃ5DbÃyF81¥lÝ)Ad‰L#.€j-0#’ Q¹e‡×·Ùq‰iVRÓe%0âEoª&‡d˜X%ƒ1à`ÀJ^tQ¯^UâÛFòËjÙ±°,æ7#1#1Q‚$H#.^#1‡,h]~r&ÿuÍè/¹ðPzê쒪מûœ›»tÚšîÑv[¥FÅÝÕé'=yx¼9yÒ¶2FáAÙGeU¶2‡kwœji¯t5yæ·[Ëf77dÞ™wzî±]ÚæfÓ´óuÞf¯27+’…n¥nO<¼Æñ]ˆH¤‰lÊŽ™)8pÚ6Ü•í·¸ÚÛÔ´e+LÛ2B”¢l¬m‹YRe)eåu®–±M¦Mia›6ÚŸ_Fß?¯FZ-P`±©‰F¶’Ú£mP•¨RlÂ-ïõúŠÑÉ:Äv(ý$nk‚Q¬K‰{v±Tkg ™ÀªKAœNMl¦ÑEmë5Ó]ZÛWÖÕk¹<€8Á}Q ö'¾ÁíÏÔB0¸ë§ëa@ÿåË Bêhø¾ÌÑ<ö¨|£ { "IÄé`±JŸPOš³,¤ÌÎ^C‹üôÌÂœ[14 ¶ª)”ò±Èƒ…§2ÄNÙU–™mÅFIמ6;£pvX^¤Úb•9'Üþ[*c÷¥Jª¢ "A²‘? ¦ÁfPFV$¤»v<BwîG§-â&·äq´™’dÙfsûö5AÇ#/‘»~h7Òոȑ7L7|7Çês,Ø#1zVÅL¦cã½W£¬P‰.n!¿Ž¾ßåz¯{SsÅ„ð+s±Ì3äc8‡HX2%lÏ$ °{¡9{žŽu^ º(<d\ˆ«7.8ÞªV¿w–›,o˜€Þ•ñd/ÐPꈎL†®[)C±h"dý¬²!+íŒîϹB\Ñ<Fˆºäœ®Hg3‘.W±‰#/Qªÿo¦#1 ^ƒš¡<}€ï‡öêK^<Z„üîêJ´ë¡Rlë“o´û Ø° Q "Úú}¿Y#1í>'ßÐÚÞ@ã ¶ªô^â7ú鹌Ê5ðý†Eˆ_½4jõzÛ[zeéÙ5£ZCø;ª*É&ó®šhͤöë\U!)7Ím2,ÂjI¬“Ùå+k’Íe±”¶“a¤ÒO¦™¤yÛ¥šdÓj)ŠYZH‰}u…#U”Ì6[41,Ú(µ©ª¦‰éÅHÕJ\ºmB_N×5×Í^Ýv4TÄ%ÌÕY‰ªõ:•i5±ic*¿u¶Ýµó;"ljõݲM‘E¬YY6ÕT‰&•MnnÚZ,™m쮓Sm½zï#1h›M¦Ê)…KXTZ–ͤÞmªV£¨ñ·EfͼêëÎäÊmiÐÕü/óiªMèWlÕãk¬e(•XHÕxMrb-K[L&%JA\þå/íŽr‡ùÖ‡‹Ù7^{(>uŸH}¥&SßßytÏ#1†Jª&ªàÄ4èGð¸¥É&C¯ºi‹Q9ý—ÞȈ_áAïm4ÃV‚à€{Ò’edë-K»¢ˆá5B;ó €a$òVÛ⥶„£àéš–›JÁ# ##.¡=üwÌÄbsT…¶Í+fš·»V®–¶*ø»5š^n×)µÊ¾~ë^SZé¨ÛnÙUÝÛZKH,Š¡P8Å1[’) /˜v¦”î0£®ò”GE ©´HE’Ûe³f%˜¡ŒddĶ¾¥tP”jRj”Ö¶›f̱¶“mCJh¥Jmô-Ê` Lµ•D´bRR4¤…M²”²&ÒFI†f2ÆÁQR‰MlÔR“i2%¤hXÙ*5”SE”²›¥-ˆ¨©M&T¥2&ÍŒZ(¬i’ɤ©%1IT†,‘jQ6£k6šT… )1`¤ÊL&™&™*Yj›c¢5dLVŠX™µ&IVÚ–k&MS%¥6Ùf¶¤Ö|uwj“iY«Md²Ù#/%¾jÞ6Ûɳf«•UÑJ…Arˆ]drdYB•¯lÛX¶µóµ¹ªRØ5¨µ´‰PÙ[_&Ú«š×¼"¸zÄ5˜á‘z¬”ÈuÐYÙ’’>Û‘·³¸RÇ a¸éŒ™ÔWËL7ö3ƒÝÒëí¸Ëþ]DÚÌ£ágAþ¤È˜¥»sÞ[P4©Rfy:ž~!kÎõTHÔ-Õ7Õ^¢HCçÈ2ó¿ˆ*Ì·€!ñ‚xú1„ôCŒvDz³ÅÜaž>noRlâ鳎ÔÉhsµ³A™~lŒõ]*›ò9)Fí%AZ´µ‹®¯’ãµíÛ˜Xÿ^a”OF-ÊúÎ\p@èSø›#1!"„—†,ª4Å:fÕµgU1ˆ‚lk‚”¨QQTAÂYô€{$™ìùPçó/Ü÷AŠ£è6ì¹¾Ž¹¨m‚uu*b‚¥~ÏùõÝQ<´Ü&øšCädS‚lë–&ÊÓe¨¹‹áçòö8ðlblÈ®(ÌÍ3¡ |!l"cÐŽhq﹊M*†ÃiØا´‘¥0à¹Lä#1¹Åp³ò0Dl£±ÇÊ=Åá·Çœž<Ocƒ9|@tP&wêtûSqóÁÑF~ÏÙ¦¼G¿Hç*^“9#1“Lª#1‹Cè©r0xU:%-?táxv𡟆øØ8íb,’3³zbœ%©èXA„¸wvÂÑô:›mŸ*6õÖŽ&EŠgk˜}<FÆÞí”q„s»PÕµEi!º}atgY÷o„Ãg6œÁ~EèD•IyEíWã²-_W/¥ïëmíÚpcR` yw#.#.cm‹jŒožjî»nCÔáìàw‚!X"ÝõÈ@*ŠT‘D¬‰Û`ë²é%•[ßivt-a~¼ëH÷Oòê;P²+ȼv¶ƒš Ón‰uháټŅóK—¶µ»áiÓ³FÜéôüÌÚvý<oÅ~ 0É2H'«@äa8y¾rêqìô†Ñ¼çîlz+³w)ƒ#,{ÇPؽ¨ÊÊÓ0>ïw;ß|N“½éyb\ÿ_÷‚Ør® v7*Å#š71~Ãד÷Þ{;9ÔWªXÙLjñ®¥â<my4_¾çÁ¼kZ•–ÅÛC+,‰‚ÐçQ4#/5§Œ) DË`V؈Ðê’LÕ…ÛEbT@X(Á+&i(ÆÓ€8Ù¦ìâšÖ8«@j©ÐùÅUcg5&Ó•Ú¤#9°eA@Ä@¦KKq‹,qKnˆc#/B¨ Š•Ût2`©jÐóH¡ú.Œªg¤5Å#sÙRh‚0k°#.Øéwó@‡²Öü‡NZH×ꃮ–hâQ‘N)¡caìØ‚x 9èù!Ã}PhE¡2Öã¥J¦²ÀY©ë“Ñraq‹@;;àtЩ]ÞÝæzœ\;œÂ±i¼E@u4NãTF99QôØ×WÊyÎx“‰Š3ðÄ*ÅâÚ–Em ¬n#1nŠ'~\4ëÕ[{ŠC¿V»/E䉘ß\úC,w*ÆÅÎÛ÷¸¿¿§êõì+ºÆ“¶i{äj\GÆÔ†Äb#1V¸ç:k#1òVÜZ|]j¬[97Éfó_#/‘©ç-%̯ͺQ©ôÇ-‰¥!I¡ž6é‚A^íIfȨJÊ™P!+~¿*8÷HV3or¾Å†ÔA áwIy#.¼½¤oÉõ“‚õôåAø½vœOƤtÑ\iƒCúìÓûHá€ò?~4„sŽoã¦=`†a™kµ€v ¡Ii#/¬ôOàZv~‘ûÚ3»ûõžÊg@acÊKPàb XÜ`-apÈC$i š””ž-DVCCpš€P±Èi“(£å3€Ü;NçÏÏ_Dì—1™Ó²ØNvZ x…áX.’e"gšxQȵãxǘdTnÇ©‘ºö÷Ã5ªA–J;©Ùó¢èÂ’B>uÄ.1µ‘ÄÆq¢^=&ˉëp§ÌÓ°î¿„UA´öq;2¡UKÑ” ÅâÔÄ©ÑFœñ»£¶1õHÐt"#.`41¹9(½#11ÜP«ÚQ48ÙX•&@ÅÌÄ”#w#~½ÆOOgwrCÏóíTšPåœoEÂê¨4x,y§o4©’u„!€²ƒ$_Ô@A’¤A))Ë‘«jžýj`TÃáÒŠÛŽ’²¡t:ñÕf¢15øSsDÁ¶úÑ!´GN#.ˆÙôeéhî—èâùó]Šp´UwM#kd1xXuÖ¨ÅÀš1yã"×a˜öAx\#/¡#/¹U›$VÀ‚#1q´£ho4\a#/`E>‡#ˆuöR»x*±}”R¯"ORõä{;ÎÝÖo+fw*wI3ïq=”d8`í:‡QE2ýJ1uµÊB¯~¼¶§–×]j^çÚxaØbÆÏ’žt©¡÷ÌK‰Ý¨x¼É˜Ÿgӂ¨q.õLeÀ†öA$B'-(Ôë7‚…ˆ¤Œ 2=l#1U^ ˆ”^o.XµŒ†7µÕêÛ˾©Õ”y/ç{8ÙËVÚîeë»Î=ÂFšmYõfÕmX£NêGôõÑÀ½|·Æº@A¾v_Þò´'J¤áTí©7C,`¬áEK¡DùkŠ‰Ž9 !lM°aZ#FÓÃ&ë»—G›§kt–isÏ:Ðxžyæwë7eç\c©]wW¤¤lŒá`Ü1ÄÄÛkÖFùÔŠ#*ªdÒ€ƒ-8 ÞLYôöw‡¯h%{ÃÆN'¯8Nåº2ù°*2A;õUÖî3Þ'p}·Cf|øqëí›Öƒ.-‰#.Ú]ˆ×v·ã#a.4W÷[fG+Xl`¶x*#%}ójØÁÈ=Eœ'1.~Ý¡ÝÜ™ENž¼ õõYóÇDéÕËÙ³¼J¨7¾Aî¼ìöå‰íä¯7¼Sp(üZtÊA;}zæë9gViKø'Õ{²` ,É=ý´‹–£Ò¦ÎÑ™EÃô¢‘-¼3‹&á‘T—TÁVFSµ…ofN/w#Ab€À³ºå–&"bÁh84ƒÉ""ï"I66$†2´‘·HÉWWŽÎmõ7”‰”É'yå\4cRDÕI$7#.8 …‰ÅTUWž§’‹½»«Ç\ë;·2\±ruØëÙ+ΚˆCQQŠØéHI¦À}û{çs!ÌQá<K©¿¿L#.m\mÑê–?·w!Ñy(ƒrèxjkrO.ÇTzä…™R×ÆiØȧk]´F Œuib0>Ba<`×꽯ٻ՘٦m¿éó.MöÀC#1èÖ:´jɽVFaÿš„,Z9Ý2h3p€[ ¡4@`Å„&Q÷wjµx°¶i¤ÓVW]7É^KÅ1%‘chÕ×åm\Æ*#V7Òµ|ô©CÁK@¨#/õeY€¤"*Eb#.7‚4uþKØ—$¢2–M\ šÂx>˜¨æ…™OÓ.C.»YœÊBÙ"ÂZ%D(@oSÞñHüX]¡Œ"ázk#/Š/˜Ù¶Æ`‡ö¾õZà?S‚£c"4Ѫ!`¦ °¯ý™šò†“–ÃQ¸Aaœ,‘¼Ðàí½rO„”T6 Ÿì‚#–¨¥boÛR„8ª¦s?Ö(*ÈÅã°©új•H̪§¶Š…],#.ÝìË“EA½Ý¹Àéçë¦Ó\Ðÿ&ê—75r³J»ºMLF”RD ’‘V&Ðßðá+Ô;õ2nìºqQp‘ûÐa˜!y#[ì\Ã1 51ñ’ÒÆNDM#1mµðå,q{¢Œˆ„E!ÜIb•ßû¯ôO¯›ïrÁamYÛª‹ Ühh0‹ #/ÄÒÝMåîöiïßÔ‰œ(¦~›ýÿ÷0‚áEÞétÚBЀ“¯ãò7c#1#/#.ZH–ç¶%Çu³W#uŸ;z·&Á¯ˆãÂ_ªFdñN!Þ(‹$Õ$†„@ëëî×h#1xÂ¥O›eÆ#1&…I#.ÎH/ÝÈœ •úýÅê#1OĽ@ˆÄ#iD½‚ŠE0Ä7\¦ßæ?k£]xI‡XãÞ£°ÄiÄ;×^`…k D2§åâøé#1¢ ¶¿*ùlH‰!^9òïTâü½þä3ú”wÃ1¯>È^mÙ¿ù å×µ®êìCaŸ˜> vº›ÁÏ”)ª)±ÌôÐüT>¯X{/ö+ÿZ'¨‚)ÆÄ¡T´‘#.OX¹µÊ«]5U¶JæµQ@Hh/lY€ vPD ‚È.j#.SPÆ<m̦["7̸ÿoÝ7c¬©]+ÚKƳ!bB‹dzLn[9„LŽó:ϞǓڧèü»‡«h1wÉö4ƒrzSÜ”tOAyYÁXOànñ‡T’@AXT¡ª ·âY7åS¸Ñ{%ªEaH®ƒKÌ ÷¤{~[¾GÏH]â•F$½}yø/líÊ·>ú #/Ÿ?¶F•ªfpÁÈâc›Ðö…Ú#1Ýçzù¶‡DòÝÃê‰æ:¶½·–úf:è‘î#/9eß—§ÄüP‰ÞE@ÁÝ#"#1X"á$#/a·`ÑòÚÀ¡¢~¸P‘GZÄ7©þxi£ëkL[Øÿ‰íƨhæ0j¤**…PS!ŸíŸ§2#18Ò+葘ÐV«h–&èL_ÊËÞ‚CFa#1°0‚QÚ?èÔÆÿÛrÝ•#1@))$Fêļd#.mfA¸ErΘvÈ’ÓX°u½ð@[I#/I1°R±@Zl˜sK@a R!v'ÌšciB8¤ÄÇH64ÕYbX<f1´61™Y+5 ¹éÅ‹Nê®m¢Ñ^ÍÍWÁµkRJB#1±a".sa-5d§ ÂbIœ,` q¥Pqj%D¡F½›ÅIËx±·»jï:ôªÒ™L6ŵtè„&5e¤Ñʈ¶Ö™Õm#/Ò6«HTº&¥éá„Ùh¬1‡-¢ÄÞª²¬ »¬ƒ6Ñß!›‚#1ÛM¢ŽNÑÍr×.m£i]Ñ¢¢ÑrÜ·1&+Îï]tȪ²S%¸Ö¤Ñ3Y¥Ù½Q5LjÓ´ŒQÆÒ+Iш»ÿ§Y†™a#Mðâ\0zFÖ?i¼_mÜ‘c,q‰º5@€áCIîÅæË4³®-‘b¦ÔBRDBÆäÕgzážÛ'÷ç¬F± u‘P2rüMÔ>€>š[m“ÓR`B扪~ŸD9;Eíi_žüíºgó•ÛS)–©+1¼uUÞuÚl.Ù¦ºÍÙkÖ±>1ü))„éHd#/³õtÊÜN\½ï_ÎÞ:*uoPG£™/6…POpqà~áú§ß‰y“i\ûjû-ðµflke,šÛKJÔ›Mmôª¯R§q¨Ü¼Ûx¶ìÔ´ÙLXÓjòµÇW';%Ët–ÆÔîºUµÔ¬kQ£j¹f͹eT–›M¦ÞÛt¶“-eå„GXÜ@X b2Ô© © (B"~%“‹ö!üÁäXwl)&"B#/#._‰÷½|Ë°lAbä꫱ËËíóïøùåiîZ,`àå÷z$4=[ Ó2!‚JQ¸ÿ®*¦€?;%mˆ%"ˆ(°eÁ¢6q›’(7]u|U“ã)ÄïåÚ8ò OYD¡ÈmBƒÈf¬˜CTÀX(a!?Û(•&AmËrù7•ãu’ÖHÚM¹Ò™R)µsnj¼VûíkË/Nˆ¢Ù5¬¤5QÓX±´@#DÂŒÈ(šÒÍU¼myœ£F,Ĥ’„"E!eBP‘T˜¥¨ |"2<ã ù÷qŽócFêh‡ùÊq”ÊŒ#.Bã}¥—IùÈ„ŠFQY‚UÁ4$3$ù:€2C#"™# ”Á±”eqV„…¶æBF„ªªªô¨x´;dp¨›BÙ>eÂ1ˆÁŽ¦íü€îŒaìó»óì`âwBXæ¹”ÞG!=ëð’ð#/Ê—Níþ‡§‡½" W«x½›M>ÓîÕ€=2\'wFèQR¯ÝÏ»·X»>°}5‹åÜG%õ‰(ZwB&Úm³Šj–L@Ø,eÏ:íO®/tT¨‚i_@b⎢ŽÛe'0ùoTN®d=€‰”–°Ý˦Õ] y„Œõ%Å<Šö‰h*D¨®4ÖE¥)j5Ñ&Ù¯y]TiZÍ6ËîÒïi]FÐF&h¬2DÛvW¯}ÎGiÀÀÎþ~6L@K£Uß¾7¶ýCT ! WA˜{ö—ìoãÞ0ü’w]QÆÇ»î/Ã&b~'¥ÌãR´£ið+wôàj“1è°úúM¦“Gk¡†´ÌãuÏÞµyL¹Î*/#`OnÏ„$ µs"ëcÉ9d˜˜ÐÊFÑ% 0ÆG{Š Åchi¼”dÙÓ;«Tî0B6`k3!‹)V\Õ*STÂÛ""˜ŽCçì`ñô#1'¡=Ox*@î$’IlG¤TöFåÈdäm='™kz·õi‘—·ð¾Î¤o®³QÈÚ&0kºäç·|–2&·Œrˆ[©»-…¨´Ô!-™?ÇíäB-ÎI&ÎŽ‚?©z™àê:J¹6sLGî±À¢…¤wbM£éõÅÓEu«k‘Œ$ˆPþ“¬F#Rš!1o:àŽM-tÛ^#/°ƒ@H¥]ŸDiB é"8@F#/ƒˆ09ÄÁ(Ê¢ã}a‘GÔžð’¿©Î#1ߟz«÷w«bdˆ›Í ÈLòä‡*}’ÆýcTkÿL,0>üò¹F3³LþžßèÜÑ]¦Å r›F‹1ź»ë±ökc:“ë†ÆNÞÁ>´õª±÷¦Íª¸ê(|¾pÙ(‰-µ—B@ŠÈ’$‰“"¨Æ²V˜bÌ©¦b¨Ú¥U-M6$Ö#13¥5ù¥v! ±™´†ÒÏÒÚܪ2V-™•M"ÑBÊj’[RŠ¦F,UM¶Ê²Ñ[52Û*UMmb¥TÖ™DÙªÍMRÑUhÖ€Œ´„4Ùï[å?uµ7‚?•úŸ¤²udð£6ÄŽê¶{0*àòÌíˆu~䢢¬‚£A*ÛE¶Æ×SZ×-»3Î*ó#1Œ¿¹ÔצÚ/§yib'à¶Opö=œ´Dä t6ÄjR¡ 6,dê_–׫ªö«_Y¥¯çËm±_Ì^ëŠûwPBpÄ¢¢æJ„ƒ#M0ÁT{P¤K>©@†–0|&GðÐukPŠB|Þ®GK£¾‘Gwí¢7‰#16ö¡çM|Óø†IP@J 5#.OÊ€<#/‡½9«¸?s2>á÷tÚD«§ßªßÛIâ¶ÔXÛoh†ÞK¦¯Wt^eW&DÚ–É!Ê•=ÈLŸ@bí¯bØÎ<Çõ|9lÓ†¼#1ßsnYºeEÁn2vº&Ä|ÐXy{*“Š#/µ!³Ž#.¾AAtch–1Þ\ÖizÌÌÁè8Ç/Ëúk)¤s#1ÂØXPµÈ…q>›ÛÜ}8‘<@ªbDfÞoP{½œíëë¹^Íì}°š€r@áRP”rãÕÝÚŒ›°Ó¤>ƒ‘„ëDC wï<†"#.AȲ~¨lÊ« ¬#/ˆß-•`as¿Ã#/VäŠZÈÁ#’àüQÓ†3F¬'¤EkCQ‚Cp‰µ‰‡ïÖIÃ8æm•EX¸°72µz^_§Ìä|ð§ÍgºïþF$Ñ‘ŠiµkñßC9#1bgU•}Ü}‘OÉ'ÝÑtpü†[Q±M=È88Ê'Qýø’À½[FAD0tűŒGD"È(ˆ¨Hƒ|qÃùmÇ–y]¿©ó^=(„„øsY[ò;·äèùæ{T*Î,Y¦ß¼åêÀêÀÍñ )“T…SB`»!`ŒVRT¤¡-#1*"ƒâ|¼M1×mñ¯Ø]òo^]ÕìFe,¬C ,R,<‹ÃMO†¸ÆŸ†ÖkG":iV"[~Wþ¡¥›5ˆ>-ÁÉHP$AHÆO(†´RUÝUÆ!qº#1¡–U+|kËÛöÜaqc|E=8 å…°M¤ÞH¤Ó—…¥²#.Á„(^µCÀÁ2BJî(ÆКէÍqo'cßsˆa¢#fL|€³…°P]Þ†Éb@/ÓJµ2)Ë:KÂ1¨vÚ™ü¥:ÇícFgM$LPû`®˜(@óæšéR; Ué*<ýÙÏß¡sÇ2õI`ŽD§³¬Mo7#/©É˜!&Õ#ƒÌ7/$Æ 4“©Õ¡/\7on†lyo9i&s#1bZ~Ñ@ïxž!(H QɹB–UXÂQ#1Jd.,Q€’§d°³‚‘ÅO4ÎÍ«ÓåKÕ. ÛµQØ;@0~O2”ûz#ëw÷ù<j¨$BMйLDRQE2QlêïY¤õ²}pþPÇ‚žÛ*Q¡®í®‚Ñ“@4Ág£-ا0úÕá£Ûæ½æÄS¯CRôvü®¶üö÷Û{x·¤m®&=ú舌ˆ†–UB1jâR#/!¶Iß-ïÉí»ìyᨅD–ª|hŠÙX—@‚È°‹!ãaqÄЧŽ’¼¹L”…Þyæ~Iy"m²°BŒàÕ·”Y>-q"!‡Èª‚€È‡YÐÆ]Új m3˜y '/KGš»”VA¨_ÒaƒÑÅ4PÛ¤$mŠq£IÎ×I2œw•P3íòæ™ØË@ìm÷‹‘c‹¹E}ÓÁ¬3@ñÄ©ñ R¥´ä1Z”¦@æK|]^Xµƒ„Ëžô²q›Ö®V•à¡T§y•ƒ)"_ãjý°ãͤ&)(¶Aó+b}Õ¶b˜Uh'‹ìe¸I/’£#/q\™!!l$V Zê—ƒ)(²ŠKS*‡K+¨Ç¼Ž’AäjYGï’¢Z'±+5žuÄü*½rT“‚sª“#]ø;oÁˆ˜4#Ï^]J¢‰•œÕ:£h|¢®ÅádôaÒ(Ñse¼D€ïÇÕôY{ß`;G²ÕˆÞÒ–%?fxòQ€ŒÓçêš(vŒÜlSõ•wE&‹†Âa°©4!¾0Ö› NHÁ#.à‡(,YFã+%5a9í*Šq†!gM·÷Óͳ£æöíƒ%N¸>T(ªÆF%U<ÌR@D¤QµW¬ÜË©¥Úñ·›-±Á.Y,Š@"'aŒa[èñ(;vS §¸€»—\nªˆoåß;~Á#/ÝeÂjdr8r(2SÔeýZš]ášG†¥8Ò+^Ížìô™»ùgÞ7;X¨duq5îö\@YYDêN;PîâŠC–…çǼùz“rÊ:–[Â@–’ vAï#.›Ÿ¢)³··Õµ\öï<4Ì›7ÜwáœM¤Eõ¾ÿ[³Y½FòÄ.¯Ží©è#.ñAŒO{CQ$|c$¡C†CÈ–Ó2b¯ç7^y·‹Îv×X••J©-V“U–‘Üê-§[måÚëÉ¢ém/:‚ÁOÏòÞo8P[2Ÿñ¸ŽŠ¹Qhz ¹(6P7#/ÞUª¢¢"Gü ßÝ%þ¯’À#pˆ#1Í*aD5ùI⽞-zrÜÞ›‘RTonégÈL“äœoçí#1nK½·¦²#/å.™pÌskm@…°ÃÁL‰q-‚ƒ`¥Jâ1rÌ ´ÈÊÄ6`™rÈ5ÂØf¨Ê²S„4ÛXE¤âŒbið!Àd`ÅQu‚XÓΞu6©7ºbÀ¬é&qB5¢#’e1œÙÓáÖ›M²;w‹#/<Co1cÆm‘µ$ #f¥à@Æ…ýÉ´è$DF ðÒðá’Ãaí1a‹êcÒ#/.È€oíMÛ#f)„B%›”lÞ¾è€O#uR'prW?S±d&&Û)DwHÈŽa4êÏzž<¾E†ÉU¿ÌG Ÿoš––.±G£Î ‹”ø–G™zâ&Éþ]îþÛ7ħS‰¢|òN ƒí¡)NÈ$€È逽±O2³IeF²VÄËFŤÖÔj”µ¾åÊ¿"ç6¤¥}{Õåj~O+ÆÑoU͹\5dH__Ûø¤FN[ÓóqÃìhéǦ唢çÆÕ0u}AÏz( =}ö³VB£~KAØvc¿©¿ŽÝÕŒc‡ÉýÂüðïꞟâ’7¹&Ƙ;©PXÈŸ:›k]®÷Ž™giÓ)ŸûòuÆ3\âb³lþ©Á½9-ªÚ[s3oËl݇½oŽÆkq´ÂI·±«€Æý™”˜Â÷ýVøð+ž>‡F³¦ùo¹‚Ùa”ÇËp:#.„Å#1&ÂF䣳'–)1{n.8×M:ÍŒ$TcÇm±å×€Q®º ÞÝ\»ºŒ´ÙI©Š@XrÏI¼8o¹at‰r÷10áøäµÏjoàóØ-¢™nKAWèRù˹o®˜l½>'‘Q½Býþzðq½¶ÝˆÜíGž~ j»"íäq¿äK&fG±àÉÍtïš;¯'NícésÇçY¢~ìYõ°»ÎË»ZQFV¦´àxú=ï˜¦Ý /¤óá‚1×c 7o=ß¿· ñ‹Î#/#æÀ¨ÈeͦӘ©¡¥„1^”n.¾mG¼Ìtí¯¾/ÔšÎ#M}$æÈÚ&ùfomŠj6WCÁ•œ‰£ v}©O®…f55véí/x|ÝÔZ1dG,¿”¾¼^‹’Æù×ݨœOæO°dÃàš„~ ÌB}ðRÛh††÷þ]tÔg„Ì/w~·¸iÅôÓ8›4•ú]]„cL>·0‰pÓêAk €[»µ„X#1UL J`¢2Ë%Ã5DôÅ#1°¸",$ÐHe$—ñ)h sCHpóÒ÷ì(Å»8lC€e@–òw.i=ÄOñ‹u•ë-årÐU%ˆ@$`ÇØmÞ,E2¢-@beyeb*, #/ÖØ@NÀ$pR8`Jb)#.´Äð¢(§ÐAXÂBñpDDlFã(›*QÆãSé½s.y½½G<ºÏbu—'fXÃõUÒ󩘵‹[rµ×.œòÅÇ’eégÓÇN˜NãfçÇ'‰ëGo²!¯Ü>#/:4ÝÔ˜!¶µ‚3$)2ÔÁ‘:WFÏ\PÃÓkˆhèt§ÝÅÈòfÖ¤·£ôÀãa”ú`@kž\ó\ÑíG<ð>®fÐX¿¸>}Á"ŠK׶%Gx‘ÌÐÛÖNºriˆQMCdlÔ„äæPI&Ú#q „h‘F‰ºÍ³MiM;v«©6Uªfµ]VÆŠæeâååÝ¥o5óM”l*mU5V±Z×áþ^#/Îäà}SËȬ°3íôø¥+M{JPS9Z±šnÅ¡ãÁ¶ â#1mÏÕÐFMñî¾#)79Ôú4ŸFn Úëô»)À©uKoÂP˜ý9IÈ65#/b˜ârڤߛDPj‚¶©>Ô ÀܶáýWcƃa±+HÖ¢F†p“d˜FYÀ{rç³7“¹¤ŠY)AŒiôçËAòþÿ˜¾ð^Po3ˆmys·nÌ£#v€åzO5μ_3zL¬+â#/ ñ9Î5wµ$€"þ'ò[Öä}\÷¸öü±™žìo-bÓ70éðâ–õo•Ã»¶zðdd#‹<—õøM¦£ì®5Öïîûy߇ü[nj«"³-#.ó•8oPŒþ)®60s'oßÊÚ8M!p<ˆ£FͯYDdPÀ<IöÃ_Éø~ÿòÿ?äßÆÐ.ÐVsŸ*—×显ۤïM{|sx&øÚÖÆCèð¤µHRœuÏu][º´4’„†Æ¨Æؘ^r#1ýf²°Xn`ëìÇ™Yøܵ„+ÉoA½´”V#1›ÀEÚ—l8ê .°)`‹ùI‡nÄðå\Rœ:¥;ž«ÌÒØÚ`õ¬h#/ÀïÝÅÇëYt'¡;”Ó¦Øá²'Ý“69lÔ@~=ÓþÈ`òŽ÷îo Œã²ŸÙû»ÕnåRI%æͶx<fƒLËm÷¨Êq¯-ú±#EÛ\&¶dkuCˆ¢Üaƒ'`í1šx #/„d.xÐ5,C§‹ÆÎ;@M‹Ûhoób0ˆÃ¼;àÎåéÒÛo·I¤"n¡æ] ˆõÀ²R`QŸ Í•Yt¨G»°…ºã«Ž@Í»÷“P»ç?~É àŞ͙7¡F#.,ƒÅ22ÈðJ®°XSoÏWîk¥"STƒô0y3ci„›rçLàÈO‡æߪ®†›!¬-0ß<jcaÓn?W4%·ýu¹5ƒQÎé#/ÅŽ#1h Â۶Ѳü-nµÏ¤µÝ£†Ð´"¼V£GÙ#.h(Ñë4Ë£åÒòÙÃM(ôð³¨^dT*¯×žAØ2`u Y';õý[_GfßQ)²˜‘‰#.à„!H¢&"`0yqS$/Ÿf·QBF@’¢:ÝMlktü*‹ÆÑZÌ´V¢-m‹V-FÔVÚY•²[clšŠYRÐ@*H)QPh†¬2–»^§Ñû÷Ýq“Ø0i;ULŒD¤é$2`ªº.A€@¥HÇ/.›Nsm÷÷ášqCyܻ婿°Á`ºwU½æ`†“j3Ó°#.¸}§QÆmƒ_ÑGhV:þÙíõ¸AõS#.ºjèy„FCvòÛ¦Û$†ŠØÓAÝ`d#1ÿÄ=±ß”^²ˆ~š{ƒâv¢i7¯iû’ F@$jzU¶~œº7)•#.¢ x mhø|80>(ïÆ/›z€{rÃùØ&~Ñi6?›³aƒs¶ûÜæ'²Ù ý‡#.ã—~š)|ÉA¨zh{Ç¢„œT„™—\æØ#/׫{°lÊ„Õ5kf•oñÚõ×L„Á³5+‡@>3Û,a&ndÁx>¸TMb•ªý²UT”y¹CÂHøwÕëÅœnæ^‘3΄£e5HL¡líI?BZ@´ÖYA#/0|@ kpƒHŠ1]Žd” ‚'üˆ‘F‚¥m5JÄÏ£MI1ë«ß1Â0È’ : Ç‘œCxbFH!³*¤EV¨TÙú>¯ƒ¾ö}9>??cúìcšò0Ä5‰‚ÆÂG!"Ár#/¶,FBj’E êžÊöç/–.Çl·%Ð{ì®Ê™%‘ÜXNWÌÕ°¬5nç*"a,•´˜ôž²pÑÚžEʨ´:ö¼†Ní¬ããœõȱh˜¾O×`v®X,@&(*¨äÇ“¾“<ún ¾ÉþËlé]ºž__¹¯â¾Ñðax\æî<ÌB‰ì=Ëpvû6—SŸ¨ìtÈõæ_ïiºôe²]PªŠÍhÒÍRâµtKVB™#1HUP¢0Bû“`×6›8>ˆÐp´óc”orLo#1@©¦Áƽ!P€hj*(A'#Cf·uÚRJ²ŒÚ*-’hÓ@:ÕÏÀ|f[k²†`À`¦„c$Æ"ŒdA¤†‡XôÀšƒ‰B7ƒhdq¸4jÚ'¡@NQ¦…S"DÂ؈˜Áê$JÅü!±‚lïã!¿›pÉꙢ~×™jÑoçzï}—Wvd–ŠÕ¾iÙ¯UÊùåtȳdÛRms¤˜Ï7v“tê¥&Ó]ëÏ'˜õ5w¿^¼h•¦#1õšìÐyuš€¨U5¨ŠÄš`Ólu™“¬Æ©™]D±´D†!ˆi¦6&Ú¥ªSPЦQ±RHiJË!%•LÄȔԽuvòí\–îw8mÉ“}#/ÛË®k4v^órÞo;Èš4*Z†Få¾]îöfÃ2#1Ù…,£ZšKÌ—YaämˆÍÆÒ#/V·Kˆ‰Rc„Ù«V@DVCqØî)«Î2iŒmæEN.©½I³ZŠ´ØÁÇôó¬æ!¦>Ú«XÂE‚Ü™l$m©´ÈW—O‡1È6ØLøÔq“i±DC‰ Lâ—áÃKƒÆSho¾”u{·^˜çLú¢òõãJíµÐHW®–5ðng]«jƬ´Â9†-9²¯0¢aV.ÎʲSB1”Ž+6Q&(b#‹Yÿ$KÈG…ÃVC05’ÂÁ›².£¤V+#/#/‹µ*7r[ÇU„aFÒbò7E´±€3-²2´(ÐM%Z#1HñÝíîÉwY4—¯]Þyã fûeÎ8Day,CV̤„–6\0ÌD8¯8¹¤Öæ¸ò•©a ‘l†"Ö+ãÆìQfjŒU½)Ê|ìF™&„ºx¾%§Yº´Ý“sJXyÀç•3[©«™ÍmÍîyª"ã#1|v´–5Œ<ý&1dô¡wÜc‰ò±,xj“skhÁÇÌeíÒá¤Ø4Á\6k1‹lÆöõBJ¦ÃÓÏHW P@9ó$q„|ŠÕÈÌÅ,Î6Ãyªþƒìe'ÕêX©v)Ðâ84gí_43h9ûþÇeÁsćm$”¸Óñ<¬/ÉÏ2|Ïnžt´›ÑÎ$ÞH¨Ìm»9a]ÈWÓq˜ö³F4Ãä0n²4°xÌi¦#/V’©µY¾2évéÇ!1)¾/Ô‹bL;g\"ѳk#/*µ˜ÈªtñÊ9§£ HF[†â#ÓÝ×ZC]ó4m1Cd8Zéü¸ôX@ÉãTè66 É̪H¤Y(*#/–ác(3*ÛlÒÔÜ_D£óÓ|ë†B›B)DT9ˆÌ0DcŠ$4¶‡dqi#/ 0)6è5Z*e#/#.Qd¥²‹!€À“DaQµÑˆƒtD[F¿ÞPzkÁëì}ÍíönO}ÿ³K2Î):'zgöD–ýÐQ‹¿ÊAí[+åø;CÜû܈ýŽKSX+åöKÓ:2Í{ëÃuŠ=uiF#1lŸµ,.–"S#1P>¯ Û(M²9X±5"{ëÒë.|%-{ö±´ß‰ÍJJW»#{º-—!Ë‹®^%gËÑ™¡bÔâ‰jY!L5MZDÖÎâaáá¦^3Y‡W4`PyãCÐDãÞ¿å?ï“<y›t8ëÔÃÇ_àý¯°”.[/;I’C“˜6£›ÑZ§æ-¨¯U~5¼õ7Î5̦¼î8ÓÈE3£‘àe<^0ž²w›£ë¡µVCdÃÍé"Ìì[žÚÛw¼ð¼8Ûa$b²ž§kÏsÞo“zÖÄÅ»ks}þF¥Ý<y÷FÍÍ÷ïx!d#U;¾5¸k'I¡—û„2òd*hüNáºx>e1¯{Ð=çÙz2…ìí+Ñ7<p”BÎ\0\¿5XܹoÍÈÃÓÉ#1öoe]4B²–Ë#/wI‚…H&Ó¡YÔÏÞvS}åF¡3ÎuØÑÎkpéqcg0Pö#/-Ãn1LU‹³=Zo÷¬>@ÈŠ0Š|¿[ŸŒwßB|Hu'ÄãAW"ÒU*š¾zý¯eb·5k…ŠÖ–å2鼶ë^^jðCå@Ú–ƒôЊȌ²›B0F£ªQÈÙ¤4;U¼t¨Þ*»-nWMjñÒÕãÎè±[¼ë›¦®Õ]6îÝ2—jñµðÍ%ˆ©‘Š•-Î;¶îí´Z“U"iE-DR¨£KE¬Ù‹ ¤#.¶@RLø8pØú²êˆ0úx7~,¬TcÔmÉ%øâO–‹Ó’-$P´A"##1`*ÀŠ…DŒ!H…1 –,¨Ø¯S „)FèRxÆË#/¡(ÈA«Ä%Ò+]¯m¯ –%²0À#10]ø¼÷Òe†±P”Cu¬×Zöm¨ŠŠÔ¤’eõû¥°h¦Í¨©Í)X¶5EF³(¨±¨Å¶$ÖCFŒišÒl‘LÐÌ™ZhѦ•šf3V¦¯;UÑ¿)#1‘ü(Ðûý4üõ(?¢*1Þk¾}µëÈ4›Tm ©$²üÇ^Ûügß鿇¡ëy-ëÓî±cøÞÞw(ß;ʃJéGs–;âËo†Œhá‡7öCÐ÷‹km¥¯ÌÖÑ©¦Ö’™F£}Žo£zëòJâÍÕÒ«ìVÛ†«O»ŠìAµoY]ŠÆ“Nî·K]Ú+®Ö±£Y#/&¨Öñ¼òÝM¥)VÓVÊõÛ³YZ5-µÊU,húìÔÝ!¼Ñw✈ęfò(ò}áA9†‚у#I!Ίl\I¼œ¹é,$2ª€²"ØÆA¡â•BÂÎÞz©¾Ô²ŽTÊðr›Â%` 1j`<YæǸ°¼d5[šOR{~{?5>aÚú¹µëòßPðÉgÉöf?{(¨…Ì]hÊŒPŸLÓp$?ÄF͇¥TK8kF4hAt”Ɇ27?E–8ØÆ2ºfI¦¡0AŒ0¹HmJÝi†d™Ã.M½(‡1PDBŒIˆˆª’]ž^'öâòyøíY„›m<&6”цLVN›>r [ú§“`”îë¤ò ѲÉø•Ò]Œ¯±Þñ¼hÀðšÅ’óa™1%äO28Ùß%µ$çÉ0ãÌBvs™ª-6݈!Bsº´@„ó¸kaŒ&S¼É“Ž²äÃH—w»ÞñM‚Éú×F0dáãx¥Î®«óØVOÚ8¯; Ø€£½MÞ6Z»OåËÊÇûê³áEÉÍŠŒZÒ–SªVÕK¡§¶IGª¼Í”QêGíPèâÉï#/BÈ=PO¦6Ù5¤¶¶5¢Ô–ß6êmbˆ# F÷1þò!•é#.2Š-‰š¼“Ó¨Û%µ±«|Í·6׌3‚B çç@'ãÝ/#ëú™›þ§pí|ù¹#>ÊD1-~voò*/?(^ÞéŒw$†DX‰" FQ+J¢(ƒP#1Œ`bô@D¹ÚxuÞϦƒ'D(Ò+T‘.a§SbÐÚŠ©À\0Wº²0…ÿHd"æá{-¾e¶¶Û2LcdÕy¬›Ïl•^wUÛö]⮲$ˆÂ%0²Ê”ÉåŠp•c#.Œd# ivµ¬Ý±á¤èV›dÈ:„-3ØÛÕ©JFAÀU¤D=XªÓ ’£cbX˜P©#1 *ˆP¨P¬V1bû‡±GL¤Í!^ fçå(#1KVDA¤r.¨ ^Ù†~ûqxŽ”„XÄA#7•$<ßæâëí•Îo#.8QòÐöÒ]~g×rþ)NMŒæf1G¼õÛZLeœÓ±bí˜<q±Âú”ÍÈ!2dʶ9Çüúcœ°SÁ^kkVü2Öª5m_eá0„¸‡`AF@Sêí<´Ïî>’(w…WÈŸæ£[ Ðømþ_ÔÔ½ k™Fè~»§SGÚÊ<XQ=ç÷‚$„dÜop~1Mõ $hú$,dâžß“}ÏÏ“OŽÇá€Xó|ù]k>nÍú”³’ñ[}èÎVv:fT8ô²îË—Œ„*,Û}Îkƒ£èDÔzb«Û¶³µ‚Ñéž™ºòÅ8d9pLGD*$–‡Øu5¸¢zÙ“‘¼ÝA9{C‰5s<êÎ8u¸`Ý–„9ÙE³#1É#/jŠáƒIAµœaÒ)#1ƒ”$ê `ÄI#.¥‚¼ª‘†Ä¥2L.ÆÁ‚Á°óú#/á€?(\!4B6+áßÇÑõôúy‚áØ_å[wvU\#VßuTøFÆrÞ滞åx¢åI½O¯DÒ´°o_‰üÐ]µCÏÂ`ðˆ*ŠÑNµv¥µNk-ÕÝŒÕõkJl¢%Ÿõ]ñ¤¡#1‹PC#’f@ìþ«žÜè8¸F”@ª–¤$TX¦S =¡fÂ<Óchû#"Ȥ׳ßwžy§yÜi¹jâiG]Þ/!±)F^W%‹,,‰LÍBl¥hòUÍKJ¤Ú))60^7Á„¹«•ËnS‡tÞo.Üu×jcåwvéݶëŹäÛ¤RÞO%º¹w5™cC®*³[FGœËTÔ¬îÜÚé·M¶ºm²–6ºÅ6îÛ´•%ot›§59™ÒÜ‘MNíÐsWYs¶V5]wZê?Ò B•´ H[X¡UàÐ#1Ybzàp²M ¤TìzøƒJâ'¸;\BÄ=#.x#ýžãÄUƒ¨B#1‚P(ìD°¶ ¨¼~äO1x'_nŠŸòÎSÞ#.=Â<.¬²)Š½Z>ÈøyB{CP×é«g–H|ý0d‡·p‡«wˆ¤ûè@ ý?Iú¼Nvò¾\Ex#1’#É! Fff›è×·5W´mµÌ´È™©kßøüb#.ˆ&“³ßÊÀ*bRTkdÕŠÕ¡KC5oš"<lo©K ö‚$J…$ŒŒ•Í®ÜêôµwŽåÎRÍ¢Å+˜K’ÄgË‹ÀÊIPO˜!»’ÉV€=>ò$’9ŒIFÝ)ç[|$ëºRRL‘EE#/ 2„6J(dµê»ÍÎÚöÏJ4‘kÃxˆ–#"ÑHÒ±f±lÁ#1º¶m·fD‹l©o~®×ªù]'ôo\ RŸ:ªVuèáE‚ÿÐˬR#.8ÃHŒÑ,bíŠøŸÍû¦ÃÝX#/‹¾Eî¹4‹Ä; +"À©«eM¥VAfÕ5U$@k~>Fj;Ò#1è#˜ªÙ4›6ÈÓi“+FÚ؇ßT‡g¤ô0Hj‘Þ|º²pD¨‘té©-37õZ´FkI1ˆC,¸¤ñ…@*-[C[Ó<(êÛAÒ;ÐöAI#1#/% ë”#.lWCÖÜ¿Aæ»EUJªÞK¾<¯¨;^‡OeÏ|ÐÛºÿª*{¾Kù'P03ý…™3ß¹ü™ð»LHÅ4ƒ”aË4kÁ±´1ƒ!#/nL¬ãˆ#/RóZ—f…¦…Ó夤$nIÁ¥FŽºîlg}É©ÖLP2"EþsX)üÕUÓÞ=‚Â`Á;Žèl9÷«h/uËPþ§Å…v3"$#.‚’%Œ+$•Š1ƒ‰Ç„…"mD¹#.l(DÓpŒc†õ!¬#1Õ²YþÅæºVæ®’Zå^ï#/‰ëu#1Öµ!*!4…XAÆc1•[¶PcCo1Ъ°¢Uˆ^*ç?Uã¦Ü©-ËÞÊÛ^$„–2E¬Ë€-µÙÀª¯²ë2äâVªù8“Ûíx¡Ý•VCúBÚÄ€º€ç#.Ê.f¤¹l®…3¹d•ë÷…h4Ö#/õÕ¬±1éƒj4J#/kR³#/taFB”EŠÔkl[ZÞ¶ÍÓrŠW³@üÿ3M‘a©»4Ó NÄiIÇ#.Ú1£DЛ®Ò¡8t*$}ƒ P>c»S„¯ÊZQ»p`ÀÇÒ XZ5r¨4J(‘ÀÕ*] ò…îW‡ª¥ª˜} íýðÑ…EøÙPy1„7#.Dš§nø4TVU#.#.± Ñ*@sUS¨ øÅ)TF•ú±¦i]z´í›kšÛÆ#/‘¨/ÎÚÁ!é.íbF"´0ŒØ(—;”öÁ৥:80‰#!î*ykÇœéü|#1Âf®¨jË-"M!#/`Ë¥—ŒýÐãa¿®¾Ö¸]?è?osXÝ>ìÌ_Ýà–CCš´H7c˜Ñ8$Z:ÓzdM·–L„™(¡ $jmWGQ—+mÙ!+¤j¬bÿnN¶ml«C;//(äGË/ÎU#1²ðªÛl+ݦµ>óyÖ½o*ši„{CD=Áîž‚[Ãq¡à”ðTöèÛ¨ÝD¨!·u¦ QØD#.‡#.¤ÞsÌN#.²?¢(#.©ˆ5E{¨i€ ˆý©0(ö#.)’€% y‚>NâÁî0lMî`9ÏÞ„#//ö™HtzÇtà>Çì·öNyxø!ÿ''ó2<^öþÞ”“F#/!°TÍ~Þ“åsg¶9‚‹°>SöAÀ/úÃ>ê?õéâÝ–cЯ£tåt©A?”iDÆÛÕÒ¼~ÝÆ·¸K.C6éµæ¨¡5!!9B4Ùûªôaƒ€e¤+Ž—WØ}¸4™¶'Þ’ÏäPé-¼Ø”j^#/ja)†"Vê¦%Ž¥fšFБ7OÔ™Ä#18åÇsBÅÝÌtðŽìÊ·¦çÉ"dL¡†‰ÕuÌŽ+f4 Öü#/.¸ÍJNš#.Ù¸F#.è*5҆ƊÙ_ £¼GÅ<TC‰`DG‰Õù:œËA³³×UT¿&‹‹p¤>!ôÑ¢92J PsC?t¡âR¨ÈŸÚ“ï÷ûû/ê<¸`Ééï{Õû^$Hû®øô»Ì¾{Í`Åü±Å§-ÃEU,u–…r‹}ܪ›±Ìù»”ø-ÐÓ³ÒÂ%þ—¥h-ëúããÅ#/ù²Nõ·)ÒZ/Ñ:lV„ºÎ+ëe¿ÍÝ-†ôi4RšXÅ4®ù^•ØO>Nª•M^nÅ)Š¥2ƒ«ˆE¤6ôÖ ,-Z&ñØcÔ!¤ÑŠšçß<8dY9¤—?žÙ.÷~|UÓ¨Fw›â(ÊÛ¥â¯<ñÕ¼®Imšws{O-;YÒži{êzÒy°0y:sbõžl†;ìÅcØÁ`‰âÚ‰áƒG?„³^]¶NÔ-îë5íÒÂœ hâžÕzâ8ïÞWržMüVa7yyQÔj‚jÆl$ìÀ饥٬K¤“ÝHovÐö÷Êk#1§U×1/Õô‰6bòÙæ®_¬¿‹…MåüìƒR+‚x½îjoËL¶Â˜n´e@¢ Š^$°XÒn,·.á>#/Ë‘ pvÓš™à™÷ó<‡´÷žD®0Äíµ«ãŒôQß{I‚«BkŽH‰ez#1ª(6åŸ=²ˆ#.•?ÎØEuBåœ=G¢Ý^Ýf_ˆ×‘Í;Ìô¹è7¦ß#.ÆÊJ;ˆp€ßu#.5ÈÃÏ#/>7µ³‰PkÞ Ô$q vÏø½9(Þn0Qƒ8v?ZÄh8GÜ}y+Qº%D¨Âx5$4r¸âQ××£·16 ³¡1[ÎM—›¶H…Þa?õ¨ú],F/‚¬Ö¨GÉ÷³*ÜeUKRª«6»{«I%€Ï½ HFظa•€m·’ɶÉsW;Õߊ_9o8Ãc+”pNæ®ÞÖ®ñ_ÍìBzOdÛºD³0ðÕõ#/o¾WÏSŽ&–»Ëz¤c¯f~ú¹ˆ±—ê,¿Lpå·q½4ã©°'•T(;ľw®þ·&p(Aíƒ3ÁXûü‘ÜcÙÏ!°™Åb¬„–0H±i¤F/‰¥ JDÐÆ3˜a?NßvgklRÓ=1U‚áP#‡3âXÙ‡tRï$øR”i@@͈`úq·j£(èÂWž½£”઄̛˜éE±{IœIv[#Ÿ~i;õËMÝ<šÞ°* —ÞÜû´IŠ*I&§l{øì<º±d룟=ØHDz}¤W{˾tÆ–¨Ñ&.ÛfYeÂbý=ª/ìL(Î|XÆq‡GÝÛðHyªž¢ .ôï•4Ú*þlÚ×¥µŒZ·äü’h‰¡±N˜ù¡ò?¬V"#1#1HÅ£F¬Q¦¶u¾÷Þ½wªÛèÙ ýyà©êÞ~Jõ¤Èffú)Ƕ!"›¨âÆ<ßÌ;ŽÚ5N®©ìôY°e·ì>dð—Ú@Wäl9°¸[¬<âd[¿2Ý`Š:ÿ´\sÆ” UãAM†áô‹#.p¥ôfRý`2‰ªŠˆÓx#.÷}«»@#½I_'ÎÀ SðÕµmóˆ44bÎi¢Šò(Xu’rPø#.uPLáâÄBâuÑ!"éÑ_$*¹sæq9Bc§PžÞaGVã¨ð£×d~b*fjûzèç«Y¢ ïå,s&—CYVŸZ³®µW'Ñ=x€©ˆŠ0ê(#1"y@(0*!¸{ûÍîSÎ㮡Žé×^ÑfNаìßµgÔ9(ø U[ŽóènñcµÛC²ƒL ÊÀ†'>;Z;©ÂדÇö¿Éõ'ŸÅö–àµ$GÓMŽ˜Ñ,'hØH>Ô¶[½Ò¡äO»ŸSF,d! Pˆ1%TˆšB$AK(lh¼!SÝP2q¢{ýò0íþ³¡ž'+Í…•MGšKó”TÌüT4̜žŸÙ%.[¢¤Kñô²»u¿|}{ß u|ª&#/ü5ÄÖ#/X|bݸ]åÔ‡/uâî®3;h^È_t}˜A™ZþË[öOîµõ"ÔéõÛ^ÃzÖw±h®´¿gKÇl„à:w>ì8L²zìfÚŠ«Ù°Å^æD߀ßpeìEBòÄ ç*6u¹cÂåZa„Že#/$>cåéû7Öô5~Zíã|ªÜ.×–àÊÁ#ÃQØ|Ñ?ÑŸ£?GäLÈHHd_Ã8ÉG‘r€ZË—X”ÇrzoÐwMëUN80X¼üû0ïß3]CœÏ7²)²Ìàý¬.#/}|—ËÄÛj×ÐïýÈ¡$ƒ³ÀÃñyŸ[1©å¢‘γè]yÛ¦>ÍkN󺋺6Ꚙ> ƒá¿«Ç”6’†Á"vµd–DÁ#/3†PØÙûø®±Ò¸Ãµ8–1j·„¼VÄ(}ÉdÆt¯CÃÉÌ4~ŠÈXòºªºy9ŽW*®Ú¤Å±ª¥³mšZ–•-m2H(FwðÝax<ëbI8!Ö¹¬“OMpm÷ù~bFè¡Pg«$Šb£i¿o÷>ç£öÿ?‡™ŒÉ3FHŠ˜CKKA1#1 ˆIF"4¦Èɱ4’"2*#1ˆ±H~rqàüS^p„4ê~7'KCä&ìŸZ÷ÑÇå¦y…åÁ‡eÝsŒ;œqÂáS£0àå6"´Æ¢3…bæñÏ"0F!Ó#.ñ#.û™Ö,¯ð—1Ëè~f9½#1ax4ÓÍý'Dï¼ù°¨^Ê‘HÔªÍEÔ`Ä»bÈyó9ªñyB„0C [2†&6ÛÆq3Òá~xlµÓIª©ÈZ´Ë1+FóØȃ!ŽC±j:Ýqµé,ýY1Øß:9Ãd¶Ðš·K²yçpû·×¤ÍÑœ·ŽA/οXc ÷8ÖŒ¹ÁšA7ÀõµˆçRH?ãd™ÉÏùuãG{ÃæB âG©Ë#3øQ@Ñk-‚©Žï/7ƒ ͹ռW«7“aoFm!–dÞ’AŒÌ©4V@îãExÙÝrR¹ª»]ÓsuIu\"Ö…ÕŠ„Œd¥3h#7͆3y…T‘Xa(4!0É#.ÂÔ0ZaŽ´‘|š"2¨bb!Q…FÝ8Ô(Ñ’$£Eý³m´-Ť֙·¸rér΂ŒÓ#.Tk‡/Ôö'ɋɬ¨ËLGÛ.X)D´] …,ÌȺB! aÊ?tpµ¹ój&¦«.¤&‹™¦¥„_ó°Â¿ƒù‹Øágˆ>5U°Í66+鬓3•QFÒažÉÊ%!ÆsˆmB6ò-´´JÛÉÀèˆó;µ³I°yìÖÍš*-œF¢!RD‹’lAŒ`‚0e\™ª¥MSDQ[J¹·rÒ"t·Šß€zsv»«w—à÷Œ0´I0ÛT¬ Dañ-1‚åÔStT6œdŽH2Œ²îî1ee¨ÄÃB‚c9Q#1á¢Zª28,Œ¨lXF6•Aª¢±7*”—(ìÂ6Êëx„!3l"®- ¢ GTŠ7"¡\Ž²Z1Žƒbl®2&R†Êæ”hÔpÇâ‰CP0lY¬³@ÞΜ縕,’R¬QwØ9QW7UrDÃZRhjXðz{ÌÚÊò-¬Þ£òDJDLe»Ú‹ì†ÎAQ&—³N¦’WGFè'bÌ¥+H;#UˆË HVI†DJL£‹Œn—‚¾°ÃÓÒÂH-÷>¢ÔÎ;ëUƒçãkX²%ÈÈÓCk³JØ“àí\dhz oPÕ4ën–bȤ$iÃjO;ƒ6Çf+AŒ– ñ&jƒpc¥FŸÌKH'…p¬ƒJ¼M)æ«c¸J—ˆ@î)ÙVX»F†Æ¬31¤,@È•FŸ#/@dWe#/D'Ú±¥5T’“#:Dδe#/# ”j’€bhóúG¾ÃžEŒË°´F5#iV€1!cL#/êŒâ’™bR¹Ö¡30¬ŠnFg7!X¡„J#MbG(Hˆâ±V#1ƒ@äA(4ŒhÞàƒÆŠ¢£ŸtÓÐp[|¡má‚mó&œå$0LiTÐD˜• ˆ˜ÄÀÇc–o£m銉½æÛ5"ˆsuºF²¢éEÜ©¼]䘛v”Rò‚QU6QR;S¡äÄM3E"‚!«s(¬O+pkt‡FÈ(ø“ÖF¶Ðµx#1JùžÆYÊŒM’ï‹ìu¦÷Â…}`N'u¤`JÛêÉÔ¥Ð6†Òª"¬u©P#ó¦"¯I飖W!~^äšìk\šÆ(ˆ–ÈMbT+(ÓÕ”P@Rtlå¥C#/D‘ˆFèÑ;¦ˆˆ$i-ÅA‰sŒ 0ÊAVñ¬m’ˆÉ¬²õ¶õnµê]©0hCHؘ Ñ•º–*ÒA’…¦¹(WbR¥‘üœC¯¨$!Uš5Þ;œ7w^ ýßåÓ£B±ABOÙE(à¤Z¨5HÈ-njo›_‹i(*Ѫ¢…©³i¢Á„au*þæUˆ!uü£Þ…)ûPÿfKÊÈz̳3¨ì…ODý\hÇh¥E¢P‚”6pD€bç†JÃú3žûÒTQù±@â:ÂzèÒÆß®J#1îHIµRV6´Ó5•¢Ûl#.á.[n—²s’'Â¥%D9UêBsuÓﳌ4¨,’¡O§áÇ=œûɃŒîMöf¬Ä®¢Æ r†·ÑŠ|’#/„#²Å#1?æbÌ‘¸Ã—wlD[]]\·¶ºé¡wpX0 œ¥Qc(PŘR6DÅj ¥PÖªTÙ‹)ˆ<’C÷ªÙr@ìH›šîÍ°b0Ù¼nÔfÚ‰(lô_ËèÖ#/€õ™²ƒ—gy,k„¼©Î_mšëÎÞßvaˆ}“’ ,F{©šnNcËFMžf«úC €o÷›¶#/ÜAòmd½êGv˜¾‘ˆàYô¶ær~;<´Jö"%nb³ìþX2$ìÜH“'sõg´ÉPì2à¼0Õ!äDÄÒl-kÔö#.#.ÞeŸÃÇå=6ž©.“Ô°³D`à¦åUãzZ[MÓ…5ÚuTú»?—¸Ýþsë>²Ì~¼Ð5ß߇¨?PÂH$}Pù“ÇO0Té×Gö~3`úç8_vê¿>ùË„ÆÓ&–)¸:¿7ꢣø#;fGö“åöùÝÉ8G=êBůÏXvÌU½µáôÂ$bˆï¹ŸZ¸é$‚‰Q ¡\~ÔîS'=š§”…Ñ]ئ!ʨœ`v¥¶ wªÀÖÂ\‰‰îŽÉbM{Ê^¸¨Tw°#/õ·pkYn±Ä¡xõ}eïĪuÎÚÈÅ<Ñ`ò&ã´°ÖQ‚T'°JÜ‚eb‚#1HŠ©ì`}©ÖZvÒ•J!µTÎß‚\Ì*{Zài–†‰q†Ÿn&,ÊCW5T†%ÛQa?cYäoƒs¤NGX&nlZ’p@)!Bj€«ýt4n©Ô…Kë¥ãUÒ.í\cRQE¯’Ö-ãnÄ·1\Ö*hÑÊæ·MºjÈkº[\¬šææ£Z¹S»Ù¹6¤Ö¿k¸h¯¥ÚÊþ‘%¸a¢C¹»¤¡»Ç„šªêÄGqFEiÇN8É™‰F©m zN[#1=)žšzÃÈØN^€àD8‘r1Hä‚ELD_°ˆš€qB>Ç裒.d E””Ä7qáÊ«ãÕüt|Ýhy¼J¨*}´@÷´*}®‘@>²#1lb2 sD²ügòÒK:˜TÐ"Í'4‡{CL’î²6<½µzÓ#1FBThb$m% #qcBp’ô}Fý‘´º¤ŒP<lFª †Z6Ô–©ekm%¶‹ºÕUºJ‚ÒÙ5ø¾}·£^·¬0¤ÈK·º<ʈp‹Uë„Š2ÍFѤÖHÑZ©-TˆFóœ|ϤTBá±Pu£ÿx= ‰¯åˆvt ì·YF-PÅ¡€}D—¥2DõÑPi†Ÿ¥8»ì«¤ö’Ž°B3¨~lóÍûO_º(²1ŠA€¨2*$‹C×ât…h,Éó"¾ª;({v¡›¡@{;ZPȲ#1–×Å]¾íZëjÿšU2›I!,ÐÁc–H2ѤسB´Í£e)2JI´hSmh¶¢Úƶ£kSiZeR™SmbØHcH@Яä3èÖä‚rJÃ2#1‘i6äÆ‚#/•Èçapت…8¹‹(¤¥¤0… 7#/‹”‘;¨4•:ѶƣÊ! X(#/lÈË €5X›C#Èå«LfJTØÂLh01@Á°t`¶ÀÄ&‚@Ô"-¨ŠR PEBª×y!Eò@IF PÆ}Ÿ³;ëk{~^Úð©x²è·MMÝÓ»1píO×Wi½uº½wfµãUÔ¨HÁK–Ù–ž€và’$ˆ»;”g„dùYüO "xTöv6ûüqåYYíÊBB¨©' >#1‰$Tˆ)äÓ_äÐÊ´YóNëñ\(#1aMvÞ:œlúZGã§UPÄüpËÊož#1˜‘&˜Øeå#.Ä0#@ðŒòã¢#/SƒöùøÖ]ÖcØÒ>-#3fˆu§a#1e-/ya¨€8þœ™9'z5}GHQ;šêgvó˼ë·tv–ò˲¦Ò™ú:áé–ÕÍFÚÅ¢ÑmmmrÖÉB¨…ª#1ÀJ%‰Ëß3<D›?½?Ò¬’ÌTöb©ž#/ºˆe +#1Jú?§M«& »8g\R;9O«hikÀe¤Uón¤A;äùâéª$ ö¤Ù>v}ÒêºrÐ4&`ZÄ3¦ë³Üÿo~׶5Ñ󤕶~Þ•m.Òº¾ù’Ö{<aÛhä‚žýnxµh—ÝL²ÖG*c–K;UG˜M×éÔ÷5ëÆÊçO-Îç[tDNÝÌ;W\•&6RÚÔ›ã¡\yPW-œï»:u•†‰9nÝg4÷^ÈTÎéã)‹ñ£ ÷°‚I†8 –$Î0ž y)ÅGU*ŸN³Òܬû;R'nîgqßT®ü¾°þ7z¶{EܼT8«áÆÝ"òšÑ4më¯3ZÃLÛmU˜"‡©fÌKâHq·ff‚Ó†òîwÍêøÖ}µVøÅ.atƒô9˜óO×#3÷yÃ?'78¥”#/$Ž'c£¶‘<iì'[lüÇ>ÿyÁ[bàñTÙ¥[¹^¹‰çGIf‘WJV<SX„›*öÆ¥0 Œ¥AµOWõ‹†µË>Z„$^<ñ^å`—\±¼Äs°K@ÑìÑÝ2€ÐM\{|âgXƒŸÐyU¬^C‰××z[&×R’…,¨I}ÑIL‡1#/R(d )‘L É’s@Â@—tþ¿ìÇ´œ…#”[^•Ôðí®=<ü76»¯x…̺mZ–£ã=ÊÏ÷öp·Ù¿Ñ™ÿ(%UÛ5`B›x+¶ÅÕx:q`è4z/•…6»2]J DãVkËJA†1qš^8È5]*P…l˶2U¥¡)è”&Ó©Z\éæ™-vÓºÌ]ZÔÙ©fOuFs©fÊg—:¼ã‘xÄb±odÔ¤ðh1XKvVNV`ÊH¢´,Š)-!(PLµM\ñŽ’[EªÚΓÈXàÞt´2’Y‹Ù};íž%å͵´ä@¦â¹©\_Ù¾ÐðbÙ¬dÂH:&f#mø«ŽðôÖúDäŽÝ‚DÏ:~̺<ê#1-¥s‹QÍZ*©UBiÈÛñÄÎs›yÐ!g5ã|(ÀÝ#.P‹ 63¢Àø‚üG&Ô#/LKèl°v6Üކ͙_5¢'O2Ã1¬zÝ3Æ“b0U£™¹YrïcÅtÛË,1¾k#.û;8‡êãÖ¼isc˜Q—³.Á´¾ØZòÎŬaÝmIݶž8s<ïñÚm°·ž:ðw”åòI=Í4qÑ'|KÒW¶k˹â]cÊ…Ë!mi9žèc0bÛ#¸è¬Îw6U<×»)ËVæ+UÞ+F"…'ªÖ‡AïP`Ò((óG#¤½ö¼³ôŲ¿ªû–º~.ÜÞ.YŽd¥ß³›;íYv…Ó_„dE,yÌÈîâs÷.þ¸µ«ê¤^Tª&pŸ½z#/ÝLÎë1Î#/ñ®7ŒžéxŽq[*év£ƒ¦YöÉ5§zI4îôþS-“³éL;»¡o]^UW|]y¾6Λ”ë+žz=˜¤ÇvæWlˆ~N×0³ÃÛ•iô#.ŒF*q(á%hªª‰ +Ïaq†ÅÏhÁL1{¬d.½f1â/×ÛÞ¹Z5ãÀW•°‰g"$Eðf¼ç7rŽWcA‘KØëN(Ýlá21«K»¹Tùç¯MõÀvçL4F´ämK4&\«¾Æìj›ØZM×w‰w¦¼t'Í®Ú ÊÊf–Û;sX¿ûßBs¤ 3%·R\»Õ2OŽ›í±¬èã뢣p|ç|)˜'Ç›ösÚ.§(jÆ#1r5ÄX3½†åÚLû6¦‹AÐÓ#/¶·tngîFèo&<c¹Æ<ð`×8M’φ¡IµúàÜÎIjŒRl2ЃíÚ:(à74¡V…„uÃ^=P™´M15œ:É1ÔŽW…ÙM—Ê\w‰f»9lÒጳE'^ÀwX)…bŠ\{½Û#.Ãõ#1ÜC'nž:9ʺy~Û¬.7Ø4bÙÁ³l6/n©ÄBÙ#.Óõ°cÓ·¼=ç–”úæ"ßàòæ#/Õ$Vž…Z{¡FSâ¸Øi{"_7(¡ZJ`Æãd#.„DR0XEL^ÊôÞq‡^NÕ\Âý{뻆™e¿KÙýd´³t€ó°‘Ó‰–fþR¥ÓD¿iªš#1š=y"\d6ݲŠDˆÝ=,6ãŽÍŽVQ…héqæxU{Ó™ïÈͳÎzd›ºèÙOù–fúM m#ÃâZ›ˆZÆV.ìRÀ-ÙTØ4 [ûI»eÃ5„(’J›v½œ$Ê4d•—ciï‹bËá<ˆ&Sìa4À‰WÉqs¢Si˜ž´4‰Q@ÎK°ôJ_F«œIeuLíé>Óͼ™[GHêˆ;¿<÷ŽªéöT{ܧap¡ûGJŠETÚQSSNGsã7®ÓÍ 1YÙl\oŠiš‡ƒi¸ÚìÍ‘ÊJ.rÞ²krŽ6àjÆMÆ47éÞ:Gé׸–""\¥ã3Í,žVW48ÕÇO³#/—sMÓ8†¥J^0˜¢b[DðYš#‡"YŽ'™œuÊú_OØŽÛbvš¬ÏYiö…Ó-¢›½áÝÔ>º‘ã íÆc ürçã‘ÅdŒuòÔ¥!À9es¼ï\wâ>‰2lu)!wñ@¹#SæìºÒ«D“#›á÷—l[˜âtq[b0ʬkêí‚)èÀøŠì¥Î™héoZk¿\B®ÍË;HqÛשë™Ç(l±\ñµ>þ æŸ.v§¯'马¼sl&‡Bîºë-ðAÊ#1}Í´N׬Ý57‡>~0xÇ(tAÜß÷êjÑï;#Öû7c&×wò·=%÷ðçb_¦j6©½]͹¯9t¸ƒ,±MzWNóè_“ê#/зè wªŸ7#/t¸„¼¤w±ã[’œÂ÷±Âg…¿GHtý:7à3\9÷ùý>בxL<üŸ‰,a*ˆ¬Œ÷¦Í¾G-J¥¬PŽÈ¸µˆ+G¬¾Ì3#.Ÿh*ˆio<Å›Ù0ažG—çÓ¡l<!ÜÁ"댬«gp}eP³×$DjXX:¶ênYÑQné½hËzú 7Ž†‹œŸ¨ï€/*ð.“ØxÙÒ®ðÃ#ÙÝ‹E¨è›6u«í«×ù˜^Âœ¬üà'íØ(C½|iĹ`C;–l\¹äm.ð åKŽ-ÞæÉ®ãó¹³d#/…9±76à¹s’cÊ\F3„Ñ´U==ÝËeR5WP¨jmÅG¡m;sFšÂò7*{íV’Í6¡]d¤%4bYL™¼ ÍÁH\q9N(W¬”ñ˜²NUÈÁÐá$ÖïXr:`³‘ªíS˜@u®uÐ÷6Pšßöõ*íNüÃ#/˜a†á˜+uP)åOø”êë7r+3E.)ñ-ÄBÔ]»²câvÙ6#ËóyypÞéÂëã‘:„ðA'›ð^wƒ¾!š8¿±1ìUT4úÿ|ëš¡ˆ—‡Z>ð÷¯qlHgÒzÂí©ßjµHJšé¿>A`'–uu#.£È€fǘ ÇJ‘q(t§sípqÚ}emQŒY‡ÖÒ¡iåldHŒDŸ|ÃÝwšÝ`í*{2hzmÝ'ê’ hq*l¶%ôš¡ £‚&ÐÁ’FÐõÕÞoWzësoMnhÚ5%&ÑnmxÞIÝrL•™ª/¯^d¦ìJ•¹x€6€D¤Ä,@Y#.[ÅIZ‚-Em‘b‡je¬ˆ¢HÁci¬ Ç…PȨˆ*Š2(#/@ìÞæ`òæs=g7ñ? Óó¸ëwÃj—’`FÜÛû9ÒcÙvÒ_ë$ΓaBüs‹zVñ[Å\ŽKÓhDõAID*(„‚#.$œ¬Ñ4Î_Uªfqò áš%Œ²é]Ož¦`ö}·xõ5fYjtªþïX«Æ6†–(Vê É”5Óë…>!›nÚçfàkF7Fiþ#©³‰ØQƒ+‰_|ÐTø³y6ÅNµ·`*ÌC<²º¸`@¥/íy#/°J1R¤… Å]R2•A¤WË>&÷zÖ®©¼k0‰Á2Ì¡LZLÝ#/I·Ù(šŒÓicRE,R±4Ó!„W-ä ÊÎC&Ü{·tÊGd’`¼¾Ùl»…¦ÓTm´FhÍ–É…AH%Èa“hLÓƒS;ĸ1LÖ\n°È›€Q§cuìdmNˆ)˜Î36/rê'54\E&LA!¸«ZzmkZ¦A¸Ck.<“µ†µux9ųo,ã&ÛIl°Tv›‘M(6¦”#/Ú6›OFhŒ˜bm¨5g#1Ûa’gR×é¾5©×9;·kL…d39Nñ+;rA0lǺµ£vJ2&8ãcF=ØñWXÅ«ÖªCD9-´È¡ÀšYŠo.e|3UŽLŽ•¬$ÍëcÆ7 —R‰ŒÔ#/7`7ºCmuñZæÒ‘'`œa§#/vÔqë»ã†•&pÈÇœ¶;É„Qšƒ0úǘ›œA•'#/[ªˆkHÐÓ9:C%i¹Œ!Ѹe)¥#1°È4n#1hpÒM615Z6óqmäÉc®»zça›[ ¹tøÐC :Ô6EJŠ×gƒCW Wki5a”j¬æ˶›¹Œc1 ìs§?ã<·Û·˜î0røê°¡´ÑMSúr²Ä7$~’^U*|ÜtëS#1˜¬Ócgi‡LÇvO#7Ûö}sj´2I Èø³qÉ æ´Ü…¦Ô°Œ!‚ÇZ1šKZ&Æc\˜O&g6Õ3s0tðáˆ((”,)†mq-R‚G¢æ˜Plô›mºÒ æ(¬þ¼¥ GÅy†0á•(øD#1101¥^äTÂ(ënº`¢ë¼#/4Œz°7`>I+Ç-4<iàæ‰"pôÛÅ…³)L8…Ù 4Ö¬ÓoLq'ÙŠXÁ–B1ØÓ<;„Œ‡>ä³gCÝ5#1êpnŽ5I€EBÌœ«s2±5»Æ‰í„á鈂ػ%Cƒ5©Î”4ÒÓÆbLd#/F¤çYˆk,à/# F°I‰2,¹J“]^æÙlÛú0ÞsÐ4øZ`üš©LŒ"0RŽÒ˺MK…Ý3Ûƒø<sÿ9½”H0#A¬Ù7OºÕûvRoÀñ®³¬D¤eR0>2mùŽhv½@>ÇÖé‰Öc¦¦«‰)ð,;ÕGƒc5ØÅ%¹¼yFÀ2ßG_í»~fÜüáØ]Üÿ¿s{z¦ìjh‡mœœ¤›8‚Lé>pI'Äï#1øŠwO#1†Éöø‡×§d$7Vw¥¨ r:ûº~ÏP:ìaÕéo«’zlf±ë'/EV:»§†ŒI#1—†mºå\À†Áq·ÙÆT>#/US7,¶‡)†}[ÍîzíÀÆð‘¡©9$cM¢"§'vìÙ»¹íôW¶üŒ’Ç„{}uÓ{ÕÔ=cë”{#.ê;ÂÀ5‰¤Mö£I#{J*U$)!÷]Sí:yväc:;¦ùè¦ T$xEÍä#1†êBäHÚñjæÞe^+%u×[¥^j™M¶ñ¶LmQu×W+Nä„6’Æ¡ R#.…R+æ.ÌÓ÷PÖiæ#Ôh”êÛÛ<;jÜOÞ†T“l)-†Æ4„dŒDÉF‘*&’…ªe¶XÛ&Á¶Ö)(ª$¬‘†3eF±HËB–l“JM’”S(„ʈÐÆ–‚¢SÑš)M¦‘J¥k$Ãl>µÆÌ"iBŒ‚©TQ$ˆ‰ûŽ]‡¨¯LÂôÁï9#/èd/¾'T‡\ïg=çä›Å÷í¹ ?#./Òu;#/8uBAœ|D9âg»ÆÚðËÚrùä¨?–jGÛŠ€*µ>ƒõª8e’à«,Fœ`Ãß&Ûò#.ä9F×µ› ´Ò´â@-Bj{(L®'ÏSð½6ݶ!ûÒïÈïHA‰„×võò2&ò»ÎÍÝÊáѹG(Á¸ÃãØ°ª¦!ã…߉òú½úÀ„ûwñ'9ßèÄ1TÊ©J©Ð¦¿Nl[zm“,‹ÚVd^ÙÌñ¥„ “oÄH#Û$@‰6Ø+HÊ#jKZ(ûí«\¬¿Ou¹WP¤ÔhÆÛÆ}WUÌey6·Z—«©©rX´‘bƒ0à0'}ââ%B@aT¡’ÙgáîÐ[4±E”žLÊ]q¿¨Í¡‹>–ŠÆb#ÈT¶ m˜Œ8Ÿ†]±¥\]<jTQÁ’´¦¥*¶6Qâ"³$6ÆÇ)#1ÍM„P°@²@´€¤#/G@L0-&#.¼ù2رhf‰s"Lìå{yÚòçhÆRº3^Ò¼¤¼nY&×]»WLu4eû09j›‚r@º.1„·”}àÓÎ"€´Ò#1Ò"µÊ ‚ddV$FS°ì£nBg3Íèz¨4%CÛ/3‰Q#/ñõÌ2º˜¸µR«#1h¨, :ëÈzÓ¹M£o™ØBf%sÃå84agö^p$F^œ²lF:6æ©%\ª50w••'wËqš“F@êˆs4„%á(Ž‚a#/@²us$¤!eÕP³D.¨Rd-1l#1Ä4”Å°H»#1…±ÉǤËÔÉ"I$‘9Ý£Pöõ_„˜S¬4Ñ0¥+–mÑ-5XH#.ìïl§ü˜ÝŽò€û#Ãèzàgû“[T>#/ðÅÀ`ŒS¿8ýYÌ]ÚFyA#/{ûij;‰¾\Ý”žÏq'æsˆîe+UP"MšÜ¸]c°Ù)äRÉ)i.Ã<¨!wH"mî#/uøB2ôLž<6•dØfEbïû(´özñÇAú“$œ÷ÄLÑ/_oŸ¬ÒHHQåˆ0PõE*m®^‹tõŸ0²4çL‰ f`ÒÂ:Äãam©"z†ÃË⟹zèiu=ÆåßÏ'ßy2VBú˜2ÓH¶ÊÃkvÒ‰JæèM`ÛL™PØ4PPSD”ÔZ±QøÄsë]kŠ<í9†[-¾ßaî°l‰¼Øg]ó8n ü>ù}·03ïV=©Ñ6¼jÍþ'ÝTÁd‚VLO5í¢›ìžÒ,yÐ’JA”BAjE¾ƒ¯aaߧ-Äó–ìñ“Cë÷}Rp]ÀnÙ˜YÍáíS#%è•EÒ|ª[MË(ø;@ÿTÍŠzž&¼Ð`ÜX©i`„ËТÈ>-aa;° Ò-š4#1Žñ¯‡×â˜ùûæ–$à<*Çž#A¶ðs!%Wãðn›&ʪ“ŸÚÓã(ž]fèùäR#$ !#.‚"#1±0iöG¡{üv¢Îòf_kø}[àÀ¢#/AÁ´#ãaódé…³êü?…æοsšb\Ÿ[¦p}ÿtl×ι¨2]4XŒd©1†ÖêÁœž°´íNg½'q¬#!Пhêà>3½““pvÐx’œ½l„€fïh¬ª¤ÂA.f%~Á^-.}µºšw{Ú&_.¢ˆ‡½•áÜÖzxÃÁƱ×äŒé°lkRÇ£œ”ˆm²Yàö†ÞÆÙâD_ey\#1¨Ð‰JwcL]ÖÒ4¨¦g…§ÖuUÃíT6UWhv…°ÓAI „Úß̉ƒ‰A#.H†§í7Ui,¿Ý**-¬‰0ß#å4x.äÖ2i¾Pc1ƒŒ‚Ê„˜y3]É‘E¤€i.âwp+X=0銰l|(C<ª/x…¡®6oœÐÁçQ œÍ¦sP3FvvÏê@úÙ¹ÑâÆcd& ØŽ 56‹ïéçOCºˆ=f¾ž¹Ø¦0c©>¤ãÓÆFxb:añe‚î¡1XIJia#.5#1#/ò™H% 13T#€Íö)A4hUå"}qx6¶M†Â#.¢È°ÛZm:È׿ž3b‚M$i ŒˆdæH$×mÛßr°r¢‚").¢!Ï:™í®üœPºíR¦„Æ9—–¡ÔYLÔŒL+Ðã{™ÛXhcA“HzM½¯gæwÝC¶ã¦#/n1ÉÚ†}jáÚL¶”âàÂ-™äÔÔꙉi"¢erL<.Ë;bñøy¼^Ë{æ%µµLO\q2™œè9ÕÇFÙaÁ&\knÊLp²f½;†ü’1Ë)¼@.Õ"T³[•”C|÷É<#/½ùp"±Ž Ñ#/Ê#1²@/®ÃY«€Ä&ˆ4…èJÂVLAÈ+DB¶RŽ ¶¼Î½Ý@¯K3)[ïºÃ5,†<£¢*"š‚-\’®®híˆD`ŠN<ƒÄÊRÊ4ÇgR£–’;ëU ¤z,do¶b9Ε 5‹º» ®Þq<¡¨GóÍ>\¼2×;¾Xy”H˜Â0¦ÙFÓ(@“Y.l€c‚Ús&´…!ßH’Út˜m*XeaLà>‡ªKK{;¤ÌI©0¶Ûm,ÅÉR=s§“™iIeó»K'Ðôå±ç¶#/Œ’úŒ‰Í.IˆfáÛpÞQ{HhM)ïjºlCl‚oOÁ¸»v4b²†o…ÌZÅÙæ£Î)²$;KÙ.žõeÖψ[m$KYq(tS¼®0»È×÷ºÂí1úEǶ[ÏR¾Ž ØR¯žÑ"¥‚ž6·8ÄÓ#1¨Þ2!äz"¦ÛìïõƒÑŇ>:»4ŽK'k…܈z%ˆ’_ ùdÒ-ŸlÉpteûúÑáabõĵUÕL¡ç]z18k¶ðŸ]`:«ÅÌ›‰¤OIÓ¹*•®:Ô*ÔE•@Œ¡®^ÊçUUmxÁX!VÐ3iÞêÜŒ•*8°€qu>®f°»žCë´#/ó†¹Ó6ÓQ•kpQO°ÒÒÂ’ÑÑww.¼,+ëEïÐDdà×&:U DTúTÊ7Ž‰„̓ oNÛ˜éÁž €ÂãgL#/*\\Îù˜lŽ°î`ÉPŸ«ÖÜÙ7N,èe;˜kŦ"B6gá.áÀàv¾Ð*¸ull`Ä…G–²…µä“rLó0…1C:i¡5 m-€¬E…TPhᆷÎ@·DË`á‚Á‹ÒEÐ"I„œùß#1:µf†Ò¼–áãbʘ¸S±®cLÎt&5õ@7Ë‹ªpÁçãÇä¹çiÍžuÈÂik#ž³Eu³¼¡Ù¦¤f†ÕLu±¬V]2L]®%¶KãLC¥ï²8ks ºŽ™´Ž'ç×ù~>NiÁÓhRä6pþy«i??Mö÷VQ$ À3Mˆ9LþŽxðæË̬y¬<uÓÈ‚›¥†§ý1éiugHÇ8Ü\³2.z˜b›ÔìùríŽIª›øŒáä˜NÙðúii:V}œ3†äÁbdtBC$µ‡(¨LP}].ÜöŸ#/ô±Î—W5(õ®^B–(h{ ó×7ìßK#1¡Q±„¶BÁQcÈltÐÍ#1ƒ¤ôf·êÒÚûDMµÛÐ;rù{xç9R¤™#/ɼ‹Ò,ô›u†Ì%…NÞV9ª~¸NÍ“~y°Ó,àœÍ5E,o¦É±¨˜ˆk»v[=F¯gMPçÌCm¸™¬Ò«fˆ[ÓË`>WRâ`—seœS±›yš‡ž.wdZd8q¢â—&GND®–årõ‰†æ#.~5šiS·^ÓŽÚ¾ ›ÓRÚ–vA=u¹8LÉS.®Ï&ݨÞ"Ó%Í9cˆç¬¡š`µÚ) b´i{UÝùkŠoº#g{|`ß.gtÇÏ5шDr.¢Epn<.GQ$$¢;nèa*ÙRèjìÚš1E Ôî5¦’*bf~‰#‹O‡l6ÔP‰y©]ßhÛ¬‚+š¥mT(ïß»œ®‚Ÿ$Ö##.VpÀæ˜(²9Ø)1·WÚæ Yˆä\ÔnÒØŠ4!€×FÅ(p7Ë‘g¦’ÛÄläXÄ4ë"Ñ¢äêfa7g`¨FO."ì·Loî‡yão{uêøT8›†ŒpfeŠ¨”`ã¦JÂ…ÇÙ™¨Ø d ÆA!R¥)R1"o¶—y¡c2–Us¢!{‰˜–7!‘[¢jBeHš,*#1w6k#/&›ÙP3 ĨoÇ’¬p„Š5ahHʹm³“6ŽPw#1¹qÔ1250›š* TCWd)ˆQƒa‘ÊÀ(\“v ‚Cs8£i3€¦ˆ¼aNyã·ÇÁ‰09p¼QtÄ»ÄÊQº±"*Ð6òÎæÕFXD-±3DT3¼4TY¨#LJ ¡…?@dß/;Ùä9ö¸PtEA”QQ@c^š¿~º*j»òÙWlžÑ¸h9üy¬Hw…DÚ¨°‰ #.-@ÕÐ#.N² sÏøϦñ@ÃÏ‘?Î>×ô¹³éUP$O·Û:æn-Œt_½ô©†ºKi}çlo¬…*ŽH„n߬}#/e1¥"l£5l‚¨ƒf^óaµw9Ÿ,ÌÏ:h )1M©R't螢à4îŸEA0l–æÞìÏYœás5Ú¤f°fšmª€Ü»!åBÈŽF¿JY¯—EU×=j䪧Ûj"þ˜&øÈË™E#1÷Ò«"/Q8T‡Q5÷VõL”R0ÒR'#.õ‹¿§4Ûû9÷³z;³ƒw)S÷UJ=åÊ‘8r°2ÅœDÈÒ*µSð²É4ÇtõщZEõ‘4É“Ç'{¶?#š´ÑçÓ|r~¶´®¤C€ˆœãh©_°. ÚG…SeõÕNźyÎÜa·4}¯{n¸Ýg1Y7zg÷Õ™CÄ'×Û_Ù` ¤TO…2ãÅùÏ¥N¾©¸H9s*Ú¶»¢w‰ñˆdgÝw|n‡ÜâuŸ>Ì:Â(?¡¤u„ªVC²m%j4R5Q¡*BD8xå1åZåMÌÈÖ;n®S´•£’¦0Ä…Úõ<ç.í¯$ÈL’™¯Ó¼nMy»´f¤WyÛל%cƒÂ#!&8å ÔrK0Ä$)oVÝ36òZ-åMzžzÝs$¨SZ4(Ðü´™#.JcÅ‘Ö$eĺ=ÁŠ˜—+¸a”Ä…4ÍűF-E-Ýh233Gb¬ÌdXâA‚¤PDü#1„Жu~¯?GÙ>ž¿#.OÁ3î=»ðæD)©éNiÖIã#.„3>‘ÍB2H²rS'Óµ¶ŠªŒ@ˆ4:HñÜÂÛ‚#/‰]’&ÉL-.wÌ´È4Hä*Y®Ò&‹`£§!(8³¬&Ñ9\‰AàÄv£É–ž/|œÄ;ŸmžŸ‚u׺D¢“µQ6öT»Íé}×&½¾æeDÎ3úåíÃø›l –¦Ç¡œ?@°óbÎ&Ý–UŠó“SŽlá°MýþìX‡ŠZ`?¤iɺ)f!PÄÄy8–åϪ4átâÅÙ1`x…ÇÍ#.ñAåçA7’äÈIE=½ÃmÝ´v‚¦ÎÌlÁæ¼RBP]03aƒƒÀâËS*ý ?9j‹.ZDcŸ—›m›]«ß¡¼ÜV*‘ŠÄOZ#1Gì0ÛÊn”ê[”UŽQqXË°3{€ý©œQ(ý•n´kîq~•¶ê¦[M$£XÛ6ÚÒ”dÛdÖ™_‚k%µìÖ¹k'+\ÚÛÞ@€b,ÀÒ#1%È¥EKØÁeZ‹"-’#1Ý"8`¡Áš*½‘à“ß2×*(ZT…‡/õà#.xš|²ÞÆœô¹¼ÛFˆÈ ,`¢ Mñw··ðžð©ïSb… ©C~7q˜ìY@DHD#.QýÄëñZîW§u%;±Q„kÁô)#/ûìtèшÍáµCÜ<WÚ•ÈìɯBN•s¹ŒYÃa E,pÎ*‚æ#/”cB#Gd¤H#1Ô¹HÕb‰#.Q‘²¸› áQ"d²:šbt-•cUe¦\¢A3LmWget««n^n×Z*žn%’E0„‚‚ŠOλ#.ò;NÞ‡o^m€q»¶„ĨÛ#.d®j¯%,-#.r¹‘f™¥>õðÇ‘ìäõ%†"§ƒññøþNË#/m‚©¾0‡O*0íEë-¡sñ,˜ËUí€ã<ŽÁÖ÷FÉà;Ý䘱@2|I(ÄÛ¯C —¦¡.ù”~M|•o¡€œ°º"È£I)‚€ÄØǦ’ ÖûÅZ#/Ûnè¤ÿ!7w´ð›,@#.-„n¨Y“:KËðé£æIŽmcïù ÇÃFs|0V!;ÄÇ9D¹ã+0:ÕwlàQ½ÑÔÔ ÞU¦ÝÔÛV¾“o‡#/ב˜,tü·âì_,`kjËÇ;8˜þ‚›dÁï†Ìè4+»ÓV6§ÛíC뇾(‰Äã¡aŠH$ SILTª ߬4k·´,³³ 鎺̧U0~soœ>óàHz—Ä'Æ"Y.€.^w/$‰Š±LÚMIJ¥•L˜Û”3k[³#/a4#FÊÌj´•~ëEµqœ×LhF,R½PHqy`›ç¥=‘ÅŠ®¼#.ÛèÚ‡Šˆr;zN©±ÌM¬ú#.†Ã~]}šÄ€è@âÛ—ng=#.ˆ$ 0røÓ£>’9'Ù¢¥¼´èîÞ%I9ó´U¼Oqó]A1UN°#1Tw1~|õé $:Ïf~|9E½õòï-î¯t•YfϯY„e.¦5J´Uæü¯_\RëåÖd5„‹Dí:)éó¸ãºF&øj†¦ô ±ÅܲYw%ƒçDÊ#/\¡ÃAäÅÇQgP8ÅNIVl‘‹ª±†L*ff¡4È8„Š¬(ƒèvã†á#/¤Í°&-8RhIKVø£k û…-àêòø»4üµ[£'kÎn¶Ñ,mµ:²pNÔ9,#12Ì-ÊÊ®XzjLÓT am‘Àý[þ¸©Äñ@=!"Ä!'xZÔàÑ}CD-d2r”kHßâh`|ïF´·XaÉÂ_˜°ülÖÉŒ².šŒ)ÉûÂ`„Š`;{ýob–K¾u¬ðëF´ªøÛÇêL (þÆ0$E㔄Ïa=á$<¹™'`xtéÓ,dtba3¼(‰Ð7¡è‚0 °!Ø1sb¢oxÇ®écç<½ÄùýE ^žg˜—…£RJ$ˆs©;Á3ŠÍÊ%|º½¾`·ÞØb%%@F¡”ƒ ꘒÞý³·Óó<û{£'URgxgÍ*B”ì6иw¥ßÕÏë“Ÿs¦¬ØÑ»n!kÉCÁ…6ê¹gŒàþå)8üÛþ¯/†úQ¨Eˆ…ÀˆÝÍá™yAIî#.üÍÛwø.òR:ö}½í».ݯ®l€0.>–l×Ììw1Í|=!s§àHX´.•>Q¸þ‹X€#.lÑ@¥^*=)µ–'nýR¶kû)½_N(ÀÉFfz먓b4i7:î¹# ˆ°cÒ‹ŠŠÒϬJnË’ñT¶Ó„””m)ei#/£D¼›¶‹y&Å¡+I´ˆcDË›¤’Å€±ˆ¥HÂ/é„!Qï?jE7 #.C$¡¼¶ý¿_Äòó”PQUò+èøXô/f{ƒÈ’³Âz>«é#.Å=}•õ~&"Ém&©VciK-¢ˆ! ÜÈÞ>ŽÑϲD$ Ä…F¥#chÒ³%SoÍÚý•ðê¾›û<4ˆ£F&ÆÃTͱV+)J”Òóöxc0b"(†Á£Ð #.´D×í„¥®\'¼H™˜Y/2)é`·Îò©#"uAÈHÕ6cMdVÊØ¢ØÙ‘‹c-¦”[4ž›Ç‹m"¡ "@‚ —€„^"ù å~Š©´ÿù:jíì_ÕþRÌÐuM>+ì=_^Ú˜û3*l¹¾í´»ƒêdÁ¼;‘)1ï#/˜(µ¡nbŽšLvè%Eó=¨>Ãñ¦«Îߦ÷°XŽÚå¶(Ûð9_ÌŸßyÑ$TßeÞÀžÄ˜ò#/ì…Å„A€Ú~Ho^¼½øEDm°6½ª›A ~¬s²&H'Dø@!BAzÅ[ÖÞ|ªóz×Ö¤ÕÍÑQ6´–QjgSe{îÞSÚñdIš·Óo¢q¶` öi ¡´"„‚4+±©ò£O"!"0åAhŒ²÷êeÙ¯_ºƒ¤>2¡^xÐb3·8õZŒç¸ÃXezsKn&3C°?’…³»8–”¥¶Î4Í}4#1éóâCyù#`š°¢ow%olÃV“4àÃþZ>¹oUðc¯j{ó¹Ü줎xdC×s3D>°ØKÐGé~p}t)üA¯Ö—ݾee/»Ù(5K4Ò¤„¤Te±ŠŠ#/DmRmP¥£_¶ûš¼a¡EhÚ“LÐlJšÉ¿Õ}ïËø|;eš¨²#1"E…a‚…Œ.ÄŒ%®¯Î7DXÂ/:)bdÂEÑ46î(Ô,”#/@=oœû#ŸÝ¶ô;î;Hˆÿp²±ZŒkAZPÕ¬Ì dU F)í;oÃɳâÎíí¥VY) ~ã_v?sIö¡c yøœãm~FšÆmïIRR™G¸ó« ü Ðvú /¼Üî?§""´¥·ó¼<ÒÈÝÛ2Ä6”pXY: F13ž—h¦ª¥d¹å¡Dà(¢ÆZyb¼ë]7Ž·Ì·Y¤ÔÑJ–õ›#1í§vmwvWy¼uM¢ÉméÉQo7ušnÌÊ®¹»j*çjl™e«Ë»i“k»«»µ²l©*dJl!YT4XXH€£%"ÂC+˵Ы•yy-³½7¯."ªSKdÔ™R¯J×kyÕמvxÖ6ÊL̪øK]l¥¯]Ü·k»u–3[.šé+–"gl6<Ù0(Ñ·h†÷™ Á«#1+ù*üª«JQhX²è_2ȨÅPLÉàHü´DsèÞס¸c†á_±‹m4ÂI͵ÁrùižxPÓŒ#1x&îUh¯{¹Ô‡d›.h·#]ßÐåû®#.y`=°\Ä~‹âTöüøÃëÌ=Þ4.(XÙÆ0(Û3|#/a¸L©M¸Cª5Vãq.$-ݯ/#íýe¦F$Ç©CUR§WÊtÎÑo5ši¦(Æû|ŒÌ=rì0û¶<â "Š8®QHÄN%ápo¯Rc Ns÷õ1´Š.Ö‘ í3`Eƒ"ÕIt3°ÝÒbz|÷˜º²"îü‹îÊIq#.ˆ;r¿yÕô:}²V]×J“×[Ø7èš°íå¸'vnxƒaZ#‘ÃL#ÞDš}ÿ q3ÚM^O-˜DPèE-ÑJ¾kU;`g2ã¶qºÉe²÷'±]#1CJ>h_ýΞi&êÀŸ–LëW¬ÍxÓ2ap5šõbQ8³ÀÕMÊ#/å›()ZN¦[-ˆL÷ä[e¸j™žôíj hìaÛ»V`¢úC#©lc¿Ð5Þ›wt«¼´Ê“h«Å¸¥(´(Œ(€QjÀ$4®?Ë9=A§<òÿJ=®Ó:9$v*µ³œ:È%ÝÎ6 ãós#/²+¤3¦KD”êl3æhè@¡]Þ3œÕ—[þ!D¹kšw©Ï2-“=–må6ÛlÊa5HÐäþ (-ˆˆ¬Ïu@«Ù·ò–ÉæëacÚ(4¿®#/¬jþJªò›Q‘hõ+lâqUÛÊœ:;ëBˆqÁŽp•fNä¤ý¤ò m60ƒL8é‡b¶ž0ÖæÁ0(ô/–XŠÒȤ¦Qh¢äu‰ ‡É‡-v1å&áô3d`%0ÆÇâhß»’ÛSòž°…“ñIÕ`‘‘ð6ÜÀèr^„ Š`øBÎgyèøhd@àæeÀǾ‘ý–üÞ$<?…¶`4ÔJÖ“î³¼óŠRè¢$û³€Z@Tü»?yä}‚nš‘¿– H\²‘°¯›€úÞ,ž™#ˆDÃb%Eà¥_Àcð»í¾à,ŽÃ¶=ZˆlT&ÏÛeVÑg÷ÿçÿåÿgý_ùvñÿÓÿ‡þ¿æþ¿ýüËÿÛfÍÝÿýÿðÿ‡þ¿òþ=¿oÑìùpÿ‡Ëðÿùø)þ~¨ÿËÿ(çâÿùxÿü?þÿáýÿãÿû¿ãÿ?‡ÿÏÍ®øáÿ.ùÿéÍà=yÛÿOýÓ»£çúþšûÔ>•ä0>Ñöæ}¿ð?:&J^ƒ¶ ’fUÃûÿŒŠ¤#À…AÞD¢áÁ²`ô¿Ôb%lȸŸàFÓ@ŸèH%QP††5étCýgùüýÎî±$&f}:ß>ÔÛ_¨ %Ë;d$ì×xAwi´C\z³#1k1eƒûlñå{+€ `DíñáÃrd:n(.l‡÷‹n·o·!ÏïS'Â/ûÒ3D ŒqŠ<\Ìv×o{Ü¿[›°T_ç#ðå÷ÚcYï̧h÷ê"¸5b·ûp)žÍqÿ óó±?âéÏ‚ÏñßÍ8ß›®éN1ïÐß>Ž—B%rŸëGG²âˆÑëßÕµ~5WºÕµék$>#/}eýP'ƒCŽ oúÚ‚EÏ‹ÿ¥ð°Œ(ßQZc˜é[U‘žElÌþÞ.ÒÀ7µ*jH†þÖF¾[©šcc[³A#ù¯2¼€Ù?D®„#/ŸÍƒFÈ8žõ‹wb髇^ÜÌ|NÑàÚR©92Ý8hix2ËÙl`…ÕÁÊ[OG,ÞŠŠ;V©°Û«N‹šË,Ѧh5r¢f¥ ºÞ¦4Ä-Òi–#1,…~On°²]É`Ë«vL„“‘•MÅ°×¾!Ø߃¼%Ó;ÉŸE*Û8ï˜Ù2#/øFò÷µ’Ñ[¨|Ðiõ×1”z¦l&ÏS€c»»#/”+ˆ¥1dƒÈÄK’$dŒN-ró¶/#e™†2áý/’#/¸™Ÿ–ZâYV~¶¬y”ë³y.‚u§HØÑ®Á’î#.±¤z.)~¹‚÷Ï)¿í?Þ¬ïöÐ4ÃÕ‰%!‰<Ž;¾Œ,vêÖj¦+½%“èÅŠF>1'‚"aèñÂ]ÂŽ3d0UÊðÉQr΢Ö)P"‚y‡½âÆà*ß&Œ¶OðWuÌUñ3ç°XÎÃ4DT@öb€FH•é»ŸŠf>µ.9s2„Œ‚k¹ß.\ÔÓþ¥[,ó@B*\ððõúOwp™›<,ØÖH#1$` ìHty…#/ù浩b×Q#/!"i“¼Îf%‰™ÃÿæB{ed€ŸH+m¹1Þ[žf#`DÂ'>{¬(èlÐÚ[Ç¥j'F›šKm¥"&6(^7QM#/¨6ÆÚ–LÛh¶JÆHÒB,(ƒ0UqJª¹çB\O->™?æŸI«r.MÒLöž5MÑVô‚‚()l‘xže‰v¥#.Åb{À›CéEþ1ÉœH²kÖ¡Ä"y··k8#.Jy«På0!þçjg'ÈÚ¾¿*ÕõâURDQä%:˜³={);?â9“—Ë%hº&zQ0‡fÙ9.ê!ö˜FUÿîŸã!”|ÄO—öû áÇZBwnP.7ÅPLÝjIÅS€tÕ\pßÂàu¢^hšà² 3³¥«Œa¡å‚??-µ>c<As£3áÕ.ä¿^^ÚusPrʼn£×[òx› 6ÍGY…ûh´j~#16mÞÙôæé[Ý&v#/#/—LüM¬Õwg®Èå*žl °R|ß;vXÖñi·Ô×5%cäÕtÔZŒoÛÓF¯e«zš6Ô›ƒZ½óZåªöW—tuîÓAL¶•ÆT¥gJÓ!Ñ®±Há¡6EqMŒþZÓ«pkÿ1@.wÕœ€Úfí#1 o<]±sÌp]îÂïHf&0M‹ñéÐ\IAÓ]áy˜å~¥óX¬÷RÑÈ(.A<?ë:®þ`TÉ@$gà:4!-ÿWÛ<|¾ÙGMEx(l~Ì_D.§÷°#1-ÉCÔ§ÊlGÿlUc#.’D@!‹#/®Ôrqá®Û±Ûk›-«%¶™šámÛUÙjù›Ù©5í{kZý(Ú‘BPrGÄ÷÷}Ÿ#@ôÙøϤM‘HA'»¯Á>¾¾Ö»ëtӸ̾VZ²q14:£¬Ô’)í‰ôKd/¿ñÍÿ`¨‡çÝÿŸðBÞ+AáxŸ4’dFü*F¿ñ›pó]?ŸþyTŸþ»ôh(ÔýÂZ»íûäß—'þ-Ÿþй‡÷ᛜá¬Yà ÿËÆÿþím^uù¼¾ï¥ØhåÎœõëŽnRt;¶<ïxðò„|—xì<å…Þ¾ï?¼Ñyšÿå¸û='oý†^TŸ®?yñÊ)›þõ/Ÿ„¼ÍÒ,†±4ŽÐo‹Ÿ»G?ü•¤ØÊ2Ó¥íÅŽ×ËÿÏï5öYuAœþ›µþ>b_Fœ*Ú&Ê?…J?ñQk8+·‰ÅQÎ ¾ÍWŒ³˜50ï‘Ä>>»N´@΃++Çœ7„nyl%ððŠ‰kÒÚs sÇãßëј:˜Ó$Éçw>ÕÞÅ$F±.>ÿ/—±%ÿrü(·MycèÈõeïÿÕX&Ò§B;bÞ©4>ckIBæÓZó×äÿ´JÔõŸÞ&%íùÞAÿü]ÉáBA£¬¯Ü +#<== diff --git a/.gitignore b/waflib/.gitignore index 8d35cb32..8d35cb32 100644 --- a/.gitignore +++ b/waflib/.gitignore diff --git a/Build.py b/waflib/Build.py index 1afcba64..1afcba64 100644 --- a/Build.py +++ b/waflib/Build.py diff --git a/waflib/COPYING b/waflib/COPYING new file mode 100644 index 00000000..a4147d2b --- /dev/null +++ b/waflib/COPYING @@ -0,0 +1,25 @@ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/ConfigSet.py b/waflib/ConfigSet.py index b300bb56..b300bb56 100644 --- a/ConfigSet.py +++ b/waflib/ConfigSet.py diff --git a/Configure.py b/waflib/Configure.py index d0a4793a..d0a4793a 100644 --- a/Configure.py +++ b/waflib/Configure.py diff --git a/Context.py b/waflib/Context.py index bb47c921..bb47c921 100644 --- a/Context.py +++ b/waflib/Context.py diff --git a/Errors.py b/waflib/Errors.py index bf75c1b6..bf75c1b6 100644 --- a/Errors.py +++ b/waflib/Errors.py diff --git a/Logs.py b/waflib/Logs.py index 2a475169..2a475169 100644 --- a/Logs.py +++ b/waflib/Logs.py diff --git a/Node.py b/waflib/Node.py index 4ac1ea8a..4ac1ea8a 100644 --- a/Node.py +++ b/waflib/Node.py diff --git a/Options.py b/waflib/Options.py index ad802d4b..ad802d4b 100644 --- a/Options.py +++ b/waflib/Options.py diff --git a/README.md b/waflib/README.md index c5361b91..c5361b91 100644 --- a/README.md +++ b/waflib/README.md diff --git a/Runner.py b/waflib/Runner.py index 261084d2..261084d2 100644 --- a/Runner.py +++ b/waflib/Runner.py diff --git a/Scripting.py b/waflib/Scripting.py index 749d4f2e..749d4f2e 100644 --- a/Scripting.py +++ b/waflib/Scripting.py diff --git a/Task.py b/waflib/Task.py index 0fc449d4..0fc449d4 100644 --- a/Task.py +++ b/waflib/Task.py diff --git a/TaskGen.py b/waflib/TaskGen.py index a74e6431..a74e6431 100644 --- a/TaskGen.py +++ b/waflib/TaskGen.py diff --git a/Tools/__init__.py b/waflib/Tools/__init__.py index 079df358..079df358 100644 --- a/Tools/__init__.py +++ b/waflib/Tools/__init__.py diff --git a/Tools/ar.py b/waflib/Tools/ar.py index b39b6459..b39b6459 100644 --- a/Tools/ar.py +++ b/waflib/Tools/ar.py diff --git a/Tools/asm.py b/waflib/Tools/asm.py index b6f26fb3..b6f26fb3 100644 --- a/Tools/asm.py +++ b/waflib/Tools/asm.py diff --git a/Tools/bison.py b/waflib/Tools/bison.py index eef56dcd..eef56dcd 100644 --- a/Tools/bison.py +++ b/waflib/Tools/bison.py diff --git a/Tools/c.py b/waflib/Tools/c.py index effd6b6e..effd6b6e 100644 --- a/Tools/c.py +++ b/waflib/Tools/c.py diff --git a/Tools/c_aliases.py b/waflib/Tools/c_aliases.py index c9d53692..c9d53692 100644 --- a/Tools/c_aliases.py +++ b/waflib/Tools/c_aliases.py diff --git a/Tools/c_config.py b/waflib/Tools/c_config.py index d2b3c0d8..d2b3c0d8 100644 --- a/Tools/c_config.py +++ b/waflib/Tools/c_config.py diff --git a/Tools/c_osx.py b/waflib/Tools/c_osx.py index f70b128b..f70b128b 100644 --- a/Tools/c_osx.py +++ b/waflib/Tools/c_osx.py diff --git a/Tools/c_preproc.py b/waflib/Tools/c_preproc.py index 7e04b4a7..7e04b4a7 100644 --- a/Tools/c_preproc.py +++ b/waflib/Tools/c_preproc.py diff --git a/Tools/c_tests.py b/waflib/Tools/c_tests.py index f858df57..f858df57 100644 --- a/Tools/c_tests.py +++ b/waflib/Tools/c_tests.py diff --git a/Tools/ccroot.py b/waflib/Tools/ccroot.py index cfef8bf5..cfef8bf5 100644 --- a/Tools/ccroot.py +++ b/waflib/Tools/ccroot.py diff --git a/Tools/clang.py b/waflib/Tools/clang.py index 3828e391..3828e391 100644 --- a/Tools/clang.py +++ b/waflib/Tools/clang.py diff --git a/Tools/clangxx.py b/waflib/Tools/clangxx.py index 152013ce..152013ce 100644 --- a/Tools/clangxx.py +++ b/waflib/Tools/clangxx.py diff --git a/Tools/compiler_c.py b/waflib/Tools/compiler_c.py index 2dba3f82..2dba3f82 100644 --- a/Tools/compiler_c.py +++ b/waflib/Tools/compiler_c.py diff --git a/Tools/compiler_cxx.py b/waflib/Tools/compiler_cxx.py index 1af65a22..1af65a22 100644 --- a/Tools/compiler_cxx.py +++ b/waflib/Tools/compiler_cxx.py diff --git a/Tools/compiler_d.py b/waflib/Tools/compiler_d.py index 43bb1f64..43bb1f64 100644 --- a/Tools/compiler_d.py +++ b/waflib/Tools/compiler_d.py diff --git a/Tools/compiler_fc.py b/waflib/Tools/compiler_fc.py index 96b58e70..96b58e70 100644 --- a/Tools/compiler_fc.py +++ b/waflib/Tools/compiler_fc.py diff --git a/Tools/cs.py b/waflib/Tools/cs.py index aecca6da..aecca6da 100644 --- a/Tools/cs.py +++ b/waflib/Tools/cs.py diff --git a/Tools/cxx.py b/waflib/Tools/cxx.py index 194fad74..194fad74 100644 --- a/Tools/cxx.py +++ b/waflib/Tools/cxx.py diff --git a/Tools/d.py b/waflib/Tools/d.py index e4cf73bb..e4cf73bb 100644 --- a/Tools/d.py +++ b/waflib/Tools/d.py diff --git a/Tools/d_config.py b/waflib/Tools/d_config.py index 66375565..66375565 100644 --- a/Tools/d_config.py +++ b/waflib/Tools/d_config.py diff --git a/Tools/d_scan.py b/waflib/Tools/d_scan.py index 14c6c313..14c6c313 100644 --- a/Tools/d_scan.py +++ b/waflib/Tools/d_scan.py diff --git a/Tools/dbus.py b/waflib/Tools/dbus.py index d520f1c0..d520f1c0 100644 --- a/Tools/dbus.py +++ b/waflib/Tools/dbus.py diff --git a/Tools/dmd.py b/waflib/Tools/dmd.py index 8917ca1b..8917ca1b 100644 --- a/Tools/dmd.py +++ b/waflib/Tools/dmd.py diff --git a/Tools/errcheck.py b/waflib/Tools/errcheck.py index de8d75a4..de8d75a4 100644 --- a/Tools/errcheck.py +++ b/waflib/Tools/errcheck.py diff --git a/Tools/fc.py b/waflib/Tools/fc.py index d9e8d8c4..d9e8d8c4 100644 --- a/Tools/fc.py +++ b/waflib/Tools/fc.py diff --git a/Tools/fc_config.py b/waflib/Tools/fc_config.py index 222f3a55..222f3a55 100644 --- a/Tools/fc_config.py +++ b/waflib/Tools/fc_config.py diff --git a/Tools/fc_scan.py b/waflib/Tools/fc_scan.py index 12cb0fc0..12cb0fc0 100644 --- a/Tools/fc_scan.py +++ b/waflib/Tools/fc_scan.py diff --git a/Tools/flex.py b/waflib/Tools/flex.py index 2256657b..2256657b 100644 --- a/Tools/flex.py +++ b/waflib/Tools/flex.py diff --git a/Tools/g95.py b/waflib/Tools/g95.py index f69ba4f3..f69ba4f3 100644 --- a/Tools/g95.py +++ b/waflib/Tools/g95.py diff --git a/Tools/gas.py b/waflib/Tools/gas.py index 77afed70..77afed70 100644 --- a/Tools/gas.py +++ b/waflib/Tools/gas.py diff --git a/Tools/gcc.py b/waflib/Tools/gcc.py index acdd473a..acdd473a 100644 --- a/Tools/gcc.py +++ b/waflib/Tools/gcc.py diff --git a/Tools/gdc.py b/waflib/Tools/gdc.py index d89a66d3..d89a66d3 100644 --- a/Tools/gdc.py +++ b/waflib/Tools/gdc.py diff --git a/Tools/gfortran.py b/waflib/Tools/gfortran.py index 10506673..10506673 100644 --- a/Tools/gfortran.py +++ b/waflib/Tools/gfortran.py diff --git a/Tools/glib2.py b/waflib/Tools/glib2.py index 949fe37c..949fe37c 100644 --- a/Tools/glib2.py +++ b/waflib/Tools/glib2.py diff --git a/Tools/gnu_dirs.py b/waflib/Tools/gnu_dirs.py index 2847071d..2847071d 100644 --- a/Tools/gnu_dirs.py +++ b/waflib/Tools/gnu_dirs.py diff --git a/Tools/gxx.py b/waflib/Tools/gxx.py index 22c5d26f..22c5d26f 100644 --- a/Tools/gxx.py +++ b/waflib/Tools/gxx.py diff --git a/Tools/icc.py b/waflib/Tools/icc.py index b6492c8e..b6492c8e 100644 --- a/Tools/icc.py +++ b/waflib/Tools/icc.py diff --git a/Tools/icpc.py b/waflib/Tools/icpc.py index 8a6cc6c4..8a6cc6c4 100644 --- a/Tools/icpc.py +++ b/waflib/Tools/icpc.py diff --git a/Tools/ifort.py b/waflib/Tools/ifort.py index 74934f3f..74934f3f 100644 --- a/Tools/ifort.py +++ b/waflib/Tools/ifort.py diff --git a/Tools/intltool.py b/waflib/Tools/intltool.py index af95ba80..af95ba80 100644 --- a/Tools/intltool.py +++ b/waflib/Tools/intltool.py diff --git a/Tools/irixcc.py b/waflib/Tools/irixcc.py index c3ae1ac9..c3ae1ac9 100644 --- a/Tools/irixcc.py +++ b/waflib/Tools/irixcc.py diff --git a/Tools/javaw.py b/waflib/Tools/javaw.py index f6fd20cc..f6fd20cc 100644 --- a/Tools/javaw.py +++ b/waflib/Tools/javaw.py diff --git a/Tools/ldc2.py b/waflib/Tools/ldc2.py index a51c344b..a51c344b 100644 --- a/Tools/ldc2.py +++ b/waflib/Tools/ldc2.py diff --git a/Tools/lua.py b/waflib/Tools/lua.py index 15a333a9..15a333a9 100644 --- a/Tools/lua.py +++ b/waflib/Tools/lua.py diff --git a/Tools/md5_tstamp.py b/waflib/Tools/md5_tstamp.py index 6428e460..6428e460 100644 --- a/Tools/md5_tstamp.py +++ b/waflib/Tools/md5_tstamp.py diff --git a/Tools/msvc.py b/waflib/Tools/msvc.py index 17b347d4..17b347d4 100644 --- a/Tools/msvc.py +++ b/waflib/Tools/msvc.py diff --git a/Tools/nasm.py b/waflib/Tools/nasm.py index 411d5826..411d5826 100644 --- a/Tools/nasm.py +++ b/waflib/Tools/nasm.py diff --git a/Tools/nobuild.py b/waflib/Tools/nobuild.py index 2e4b055e..2e4b055e 100644 --- a/Tools/nobuild.py +++ b/waflib/Tools/nobuild.py diff --git a/Tools/perl.py b/waflib/Tools/perl.py index 32b03fba..32b03fba 100644 --- a/Tools/perl.py +++ b/waflib/Tools/perl.py diff --git a/Tools/python.py b/waflib/Tools/python.py index 25841d03..25841d03 100644 --- a/Tools/python.py +++ b/waflib/Tools/python.py diff --git a/Tools/qt5.py b/waflib/Tools/qt5.py index 4f9c6908..4f9c6908 100644 --- a/Tools/qt5.py +++ b/waflib/Tools/qt5.py diff --git a/Tools/ruby.py b/waflib/Tools/ruby.py index 8d92a79a..8d92a79a 100644 --- a/Tools/ruby.py +++ b/waflib/Tools/ruby.py diff --git a/Tools/suncc.py b/waflib/Tools/suncc.py index 33d34fc9..33d34fc9 100644 --- a/Tools/suncc.py +++ b/waflib/Tools/suncc.py diff --git a/Tools/suncxx.py b/waflib/Tools/suncxx.py index 3b384f6f..3b384f6f 100644 --- a/Tools/suncxx.py +++ b/waflib/Tools/suncxx.py diff --git a/Tools/tex.py b/waflib/Tools/tex.py index eaf9fdb5..eaf9fdb5 100644 --- a/Tools/tex.py +++ b/waflib/Tools/tex.py diff --git a/Tools/vala.py b/waflib/Tools/vala.py index 822ec502..822ec502 100644 --- a/Tools/vala.py +++ b/waflib/Tools/vala.py diff --git a/Tools/waf_unit_test.py b/waflib/Tools/waf_unit_test.py index a71ed1c0..a71ed1c0 100644 --- a/Tools/waf_unit_test.py +++ b/waflib/Tools/waf_unit_test.py diff --git a/Tools/winres.py b/waflib/Tools/winres.py index 586c596c..586c596c 100644 --- a/Tools/winres.py +++ b/waflib/Tools/winres.py diff --git a/Tools/xlc.py b/waflib/Tools/xlc.py index 134dd415..134dd415 100644 --- a/Tools/xlc.py +++ b/waflib/Tools/xlc.py diff --git a/Tools/xlcxx.py b/waflib/Tools/xlcxx.py index 76aa59bc..76aa59bc 100644 --- a/Tools/xlcxx.py +++ b/waflib/Tools/xlcxx.py diff --git a/Utils.py b/waflib/Utils.py index b4665c4d..b4665c4d 100644 --- a/Utils.py +++ b/waflib/Utils.py diff --git a/__init__.py b/waflib/__init__.py index 079df358..079df358 100644 --- a/__init__.py +++ b/waflib/__init__.py diff --git a/ansiterm.py b/waflib/ansiterm.py index 0d20c637..0d20c637 100644 --- a/ansiterm.py +++ b/waflib/ansiterm.py diff --git a/extras/__init__.py b/waflib/extras/__init__.py index c8a3c349..c8a3c349 100644 --- a/extras/__init__.py +++ b/waflib/extras/__init__.py diff --git a/extras/autowaf.py b/waflib/extras/autowaf.py index a4a06ba8..a4a06ba8 100644 --- a/extras/autowaf.py +++ b/waflib/extras/autowaf.py diff --git a/extras/batched_cc.py b/waflib/extras/batched_cc.py index aad28722..aad28722 100644 --- a/extras/batched_cc.py +++ b/waflib/extras/batched_cc.py diff --git a/extras/biber.py b/waflib/extras/biber.py index fd9db4e0..fd9db4e0 100644 --- a/extras/biber.py +++ b/waflib/extras/biber.py diff --git a/extras/bjam.py b/waflib/extras/bjam.py index 8e04d3a6..8e04d3a6 100644 --- a/extras/bjam.py +++ b/waflib/extras/bjam.py diff --git a/extras/blender.py b/waflib/extras/blender.py index e5efc280..e5efc280 100644 --- a/extras/blender.py +++ b/waflib/extras/blender.py diff --git a/extras/boo.py b/waflib/extras/boo.py index 06623d40..06623d40 100644 --- a/extras/boo.py +++ b/waflib/extras/boo.py diff --git a/extras/boost.py b/waflib/extras/boost.py index c2aaaa93..c2aaaa93 100644 --- a/extras/boost.py +++ b/waflib/extras/boost.py diff --git a/extras/build_file_tracker.py b/waflib/extras/build_file_tracker.py index c4f26fd0..c4f26fd0 100644 --- a/extras/build_file_tracker.py +++ b/waflib/extras/build_file_tracker.py diff --git a/extras/build_logs.py b/waflib/extras/build_logs.py index cdf8ed09..cdf8ed09 100644 --- a/extras/build_logs.py +++ b/waflib/extras/build_logs.py diff --git a/extras/buildcopy.py b/waflib/extras/buildcopy.py index a6d9ac83..a6d9ac83 100644 --- a/extras/buildcopy.py +++ b/waflib/extras/buildcopy.py diff --git a/extras/c_bgxlc.py b/waflib/extras/c_bgxlc.py index 6e3eaf7b..6e3eaf7b 100644 --- a/extras/c_bgxlc.py +++ b/waflib/extras/c_bgxlc.py diff --git a/extras/c_dumbpreproc.py b/waflib/extras/c_dumbpreproc.py index ce9e1a40..ce9e1a40 100644 --- a/extras/c_dumbpreproc.py +++ b/waflib/extras/c_dumbpreproc.py diff --git a/extras/c_emscripten.py b/waflib/extras/c_emscripten.py index e1ac494f..e1ac494f 100644 --- a/extras/c_emscripten.py +++ b/waflib/extras/c_emscripten.py diff --git a/extras/c_nec.py b/waflib/extras/c_nec.py index 96bfae4f..96bfae4f 100644 --- a/extras/c_nec.py +++ b/waflib/extras/c_nec.py diff --git a/extras/cabal.py b/waflib/extras/cabal.py index e10a0d11..e10a0d11 100644 --- a/extras/cabal.py +++ b/waflib/extras/cabal.py diff --git a/extras/cfg_altoptions.py b/waflib/extras/cfg_altoptions.py index 47b1189f..47b1189f 100644 --- a/extras/cfg_altoptions.py +++ b/waflib/extras/cfg_altoptions.py diff --git a/extras/clang_compilation_database.py b/waflib/extras/clang_compilation_database.py index 4d9b5e27..4d9b5e27 100644 --- a/extras/clang_compilation_database.py +++ b/waflib/extras/clang_compilation_database.py diff --git a/extras/codelite.py b/waflib/extras/codelite.py index 523302c0..523302c0 100644 --- a/extras/codelite.py +++ b/waflib/extras/codelite.py diff --git a/extras/color_gcc.py b/waflib/extras/color_gcc.py index b68c5ebf..b68c5ebf 100644 --- a/extras/color_gcc.py +++ b/waflib/extras/color_gcc.py diff --git a/extras/color_rvct.py b/waflib/extras/color_rvct.py index f89ccbdb..f89ccbdb 100644 --- a/extras/color_rvct.py +++ b/waflib/extras/color_rvct.py diff --git a/extras/compat15.py b/waflib/extras/compat15.py index 0e74df85..0e74df85 100644 --- a/extras/compat15.py +++ b/waflib/extras/compat15.py diff --git a/extras/cppcheck.py b/waflib/extras/cppcheck.py index 13ff4247..13ff4247 100644 --- a/extras/cppcheck.py +++ b/waflib/extras/cppcheck.py diff --git a/extras/cpplint.py b/waflib/extras/cpplint.py index e3302e5b..e3302e5b 100644 --- a/extras/cpplint.py +++ b/waflib/extras/cpplint.py diff --git a/extras/cross_gnu.py b/waflib/extras/cross_gnu.py index 309f53b0..309f53b0 100644 --- a/extras/cross_gnu.py +++ b/waflib/extras/cross_gnu.py diff --git a/extras/cython.py b/waflib/extras/cython.py index 481d6f4c..481d6f4c 100644 --- a/extras/cython.py +++ b/waflib/extras/cython.py diff --git a/extras/dcc.py b/waflib/extras/dcc.py index c1a57c04..c1a57c04 100644 --- a/extras/dcc.py +++ b/waflib/extras/dcc.py diff --git a/extras/distnet.py b/waflib/extras/distnet.py index 09a31a6d..09a31a6d 100644 --- a/extras/distnet.py +++ b/waflib/extras/distnet.py diff --git a/extras/doxygen.py b/waflib/extras/doxygen.py index 28f56e9c..28f56e9c 100644 --- a/extras/doxygen.py +++ b/waflib/extras/doxygen.py diff --git a/extras/dpapi.py b/waflib/extras/dpapi.py index b94d4823..b94d4823 100644 --- a/extras/dpapi.py +++ b/waflib/extras/dpapi.py diff --git a/extras/eclipse.py b/waflib/extras/eclipse.py index bb787416..bb787416 100644 --- a/extras/eclipse.py +++ b/waflib/extras/eclipse.py diff --git a/extras/erlang.py b/waflib/extras/erlang.py index 49f6d5b4..49f6d5b4 100644 --- a/extras/erlang.py +++ b/waflib/extras/erlang.py diff --git a/extras/fast_partial.py b/waflib/extras/fast_partial.py index b3af513b..b3af513b 100644 --- a/extras/fast_partial.py +++ b/waflib/extras/fast_partial.py diff --git a/extras/fc_bgxlf.py b/waflib/extras/fc_bgxlf.py index cca18101..cca18101 100644 --- a/extras/fc_bgxlf.py +++ b/waflib/extras/fc_bgxlf.py diff --git a/extras/fc_cray.py b/waflib/extras/fc_cray.py index da733fad..da733fad 100644 --- a/extras/fc_cray.py +++ b/waflib/extras/fc_cray.py diff --git a/extras/fc_nag.py b/waflib/extras/fc_nag.py index edcb218b..edcb218b 100644 --- a/extras/fc_nag.py +++ b/waflib/extras/fc_nag.py diff --git a/extras/fc_nec.py b/waflib/extras/fc_nec.py index 67c86808..67c86808 100644 --- a/extras/fc_nec.py +++ b/waflib/extras/fc_nec.py diff --git a/extras/fc_open64.py b/waflib/extras/fc_open64.py index 413719f4..413719f4 100644 --- a/extras/fc_open64.py +++ b/waflib/extras/fc_open64.py diff --git a/extras/fc_pgfortran.py b/waflib/extras/fc_pgfortran.py index afb2817b..afb2817b 100644 --- a/extras/fc_pgfortran.py +++ b/waflib/extras/fc_pgfortran.py diff --git a/extras/fc_solstudio.py b/waflib/extras/fc_solstudio.py index 53766df8..53766df8 100644 --- a/extras/fc_solstudio.py +++ b/waflib/extras/fc_solstudio.py diff --git a/extras/fc_xlf.py b/waflib/extras/fc_xlf.py index 5a3da034..5a3da034 100644 --- a/extras/fc_xlf.py +++ b/waflib/extras/fc_xlf.py diff --git a/extras/file_to_object.py b/waflib/extras/file_to_object.py index 1393b511..1393b511 100644 --- a/extras/file_to_object.py +++ b/waflib/extras/file_to_object.py diff --git a/extras/fluid.py b/waflib/extras/fluid.py index 4814a35b..4814a35b 100644 --- a/extras/fluid.py +++ b/waflib/extras/fluid.py diff --git a/extras/freeimage.py b/waflib/extras/freeimage.py index f27e5258..f27e5258 100644 --- a/extras/freeimage.py +++ b/waflib/extras/freeimage.py diff --git a/extras/fsb.py b/waflib/extras/fsb.py index 1b8f398f..1b8f398f 100644 --- a/extras/fsb.py +++ b/waflib/extras/fsb.py diff --git a/extras/fsc.py b/waflib/extras/fsc.py index c67e70be..c67e70be 100644 --- a/extras/fsc.py +++ b/waflib/extras/fsc.py diff --git a/extras/gccdeps.py b/waflib/extras/gccdeps.py index d9758ab3..d9758ab3 100644 --- a/extras/gccdeps.py +++ b/waflib/extras/gccdeps.py diff --git a/extras/gdbus.py b/waflib/extras/gdbus.py index 0e0476e3..0e0476e3 100644 --- a/extras/gdbus.py +++ b/waflib/extras/gdbus.py diff --git a/extras/gob2.py b/waflib/extras/gob2.py index b4fa3b9a..b4fa3b9a 100644 --- a/extras/gob2.py +++ b/waflib/extras/gob2.py diff --git a/extras/halide.py b/waflib/extras/halide.py index 6078e38b..6078e38b 100644 --- a/extras/halide.py +++ b/waflib/extras/halide.py diff --git a/extras/javatest.py b/waflib/extras/javatest.py index 979b8d82..979b8d82 100755 --- a/extras/javatest.py +++ b/waflib/extras/javatest.py diff --git a/extras/kde4.py b/waflib/extras/kde4.py index e49a9ec0..e49a9ec0 100644 --- a/extras/kde4.py +++ b/waflib/extras/kde4.py diff --git a/extras/local_rpath.py b/waflib/extras/local_rpath.py index b2507e17..b2507e17 100644 --- a/extras/local_rpath.py +++ b/waflib/extras/local_rpath.py diff --git a/extras/lv2.py b/waflib/extras/lv2.py index 815987fc..815987fc 100644 --- a/extras/lv2.py +++ b/waflib/extras/lv2.py diff --git a/extras/make.py b/waflib/extras/make.py index 933d9cac..933d9cac 100644 --- a/extras/make.py +++ b/waflib/extras/make.py diff --git a/extras/midl.py b/waflib/extras/midl.py index 43e6cf91..43e6cf91 100644 --- a/extras/midl.py +++ b/waflib/extras/midl.py diff --git a/extras/msvcdeps.py b/waflib/extras/msvcdeps.py index fc1ecd4d..fc1ecd4d 100644 --- a/extras/msvcdeps.py +++ b/waflib/extras/msvcdeps.py diff --git a/extras/msvs.py b/waflib/extras/msvs.py index 8aa2db0b..8aa2db0b 100644 --- a/extras/msvs.py +++ b/waflib/extras/msvs.py diff --git a/extras/netcache_client.py b/waflib/extras/netcache_client.py index dc490485..dc490485 100644 --- a/extras/netcache_client.py +++ b/waflib/extras/netcache_client.py diff --git a/extras/objcopy.py b/waflib/extras/objcopy.py index 82d8359e..82d8359e 100644 --- a/extras/objcopy.py +++ b/waflib/extras/objcopy.py diff --git a/extras/ocaml.py b/waflib/extras/ocaml.py index afe73c0c..afe73c0c 100644 --- a/extras/ocaml.py +++ b/waflib/extras/ocaml.py diff --git a/extras/package.py b/waflib/extras/package.py index c06498eb..c06498eb 100644 --- a/extras/package.py +++ b/waflib/extras/package.py diff --git a/extras/parallel_debug.py b/waflib/extras/parallel_debug.py index 35883a3d..35883a3d 100644 --- a/extras/parallel_debug.py +++ b/waflib/extras/parallel_debug.py diff --git a/extras/pch.py b/waflib/extras/pch.py index 103e7528..103e7528 100644 --- a/extras/pch.py +++ b/waflib/extras/pch.py diff --git a/extras/pep8.py b/waflib/extras/pep8.py index 676beedb..676beedb 100644 --- a/extras/pep8.py +++ b/waflib/extras/pep8.py diff --git a/extras/pgicc.py b/waflib/extras/pgicc.py index 9790b9cf..9790b9cf 100644 --- a/extras/pgicc.py +++ b/waflib/extras/pgicc.py diff --git a/extras/pgicxx.py b/waflib/extras/pgicxx.py index eae121c4..eae121c4 100644 --- a/extras/pgicxx.py +++ b/waflib/extras/pgicxx.py diff --git a/extras/proc.py b/waflib/extras/proc.py index 764abecf..764abecf 100644 --- a/extras/proc.py +++ b/waflib/extras/proc.py diff --git a/extras/protoc.py b/waflib/extras/protoc.py index f3cb4d86..f3cb4d86 100644 --- a/extras/protoc.py +++ b/waflib/extras/protoc.py diff --git a/extras/pyqt5.py b/waflib/extras/pyqt5.py index c21dfa72..c21dfa72 100644 --- a/extras/pyqt5.py +++ b/waflib/extras/pyqt5.py diff --git a/extras/pytest.py b/waflib/extras/pytest.py index 7dd5a1a0..7dd5a1a0 100644 --- a/extras/pytest.py +++ b/waflib/extras/pytest.py diff --git a/extras/qnxnto.py b/waflib/extras/qnxnto.py index 1158124d..1158124d 100644 --- a/extras/qnxnto.py +++ b/waflib/extras/qnxnto.py diff --git a/extras/qt4.py b/waflib/extras/qt4.py index 90cae7e0..90cae7e0 100644 --- a/extras/qt4.py +++ b/waflib/extras/qt4.py diff --git a/extras/relocation.py b/waflib/extras/relocation.py index 7e821f41..7e821f41 100644 --- a/extras/relocation.py +++ b/waflib/extras/relocation.py diff --git a/extras/remote.py b/waflib/extras/remote.py index 3b038f77..3b038f77 100644 --- a/extras/remote.py +++ b/waflib/extras/remote.py diff --git a/extras/resx.py b/waflib/extras/resx.py index caf4d318..caf4d318 100644 --- a/extras/resx.py +++ b/waflib/extras/resx.py diff --git a/extras/review.py b/waflib/extras/review.py index 561e0621..561e0621 100644 --- a/extras/review.py +++ b/waflib/extras/review.py diff --git a/extras/rst.py b/waflib/extras/rst.py index f3c3a5eb..f3c3a5eb 100644 --- a/extras/rst.py +++ b/waflib/extras/rst.py diff --git a/extras/run_do_script.py b/waflib/extras/run_do_script.py index f3c58122..f3c58122 100644 --- a/extras/run_do_script.py +++ b/waflib/extras/run_do_script.py diff --git a/extras/run_m_script.py b/waflib/extras/run_m_script.py index b5f27ebe..b5f27ebe 100644 --- a/extras/run_m_script.py +++ b/waflib/extras/run_m_script.py diff --git a/extras/run_py_script.py b/waflib/extras/run_py_script.py index 36703811..36703811 100644 --- a/extras/run_py_script.py +++ b/waflib/extras/run_py_script.py diff --git a/extras/run_r_script.py b/waflib/extras/run_r_script.py index b0d8f2b2..b0d8f2b2 100644 --- a/extras/run_r_script.py +++ b/waflib/extras/run_r_script.py diff --git a/extras/sas.py b/waflib/extras/sas.py index 754c6148..754c6148 100644 --- a/extras/sas.py +++ b/waflib/extras/sas.py diff --git a/extras/satellite_assembly.py b/waflib/extras/satellite_assembly.py index 005eb074..005eb074 100644 --- a/extras/satellite_assembly.py +++ b/waflib/extras/satellite_assembly.py diff --git a/extras/scala.py b/waflib/extras/scala.py index a9880f02..a9880f02 100644 --- a/extras/scala.py +++ b/waflib/extras/scala.py diff --git a/extras/slow_qt4.py b/waflib/extras/slow_qt4.py index ec7880bf..ec7880bf 100644 --- a/extras/slow_qt4.py +++ b/waflib/extras/slow_qt4.py diff --git a/extras/softlink_libs.py b/waflib/extras/softlink_libs.py index 50c777f2..50c777f2 100644 --- a/extras/softlink_libs.py +++ b/waflib/extras/softlink_libs.py diff --git a/extras/stale.py b/waflib/extras/stale.py index cac3f469..cac3f469 100644 --- a/extras/stale.py +++ b/waflib/extras/stale.py diff --git a/extras/stracedeps.py b/waflib/extras/stracedeps.py index 37d82cbb..37d82cbb 100644 --- a/extras/stracedeps.py +++ b/waflib/extras/stracedeps.py diff --git a/extras/swig.py b/waflib/extras/swig.py index fd3d6d2c..fd3d6d2c 100644 --- a/extras/swig.py +++ b/waflib/extras/swig.py diff --git a/extras/syms.py b/waflib/extras/syms.py index dfa00593..dfa00593 100644 --- a/extras/syms.py +++ b/waflib/extras/syms.py diff --git a/extras/ticgt.py b/waflib/extras/ticgt.py index f43a7ea5..f43a7ea5 100644 --- a/extras/ticgt.py +++ b/waflib/extras/ticgt.py diff --git a/extras/unity.py b/waflib/extras/unity.py index 78128ed3..78128ed3 100644 --- a/extras/unity.py +++ b/waflib/extras/unity.py diff --git a/extras/use_config.py b/waflib/extras/use_config.py index ef5129f2..ef5129f2 100644 --- a/extras/use_config.py +++ b/waflib/extras/use_config.py diff --git a/extras/valadoc.py b/waflib/extras/valadoc.py index c50f69e7..c50f69e7 100644 --- a/extras/valadoc.py +++ b/waflib/extras/valadoc.py diff --git a/extras/waf_xattr.py b/waflib/extras/waf_xattr.py index 351dd63a..351dd63a 100644 --- a/extras/waf_xattr.py +++ b/waflib/extras/waf_xattr.py diff --git a/extras/why.py b/waflib/extras/why.py index 1bb941f6..1bb941f6 100644 --- a/extras/why.py +++ b/waflib/extras/why.py diff --git a/extras/win32_opts.py b/waflib/extras/win32_opts.py index 9f7443c3..9f7443c3 100644 --- a/extras/win32_opts.py +++ b/waflib/extras/win32_opts.py diff --git a/extras/wix.py b/waflib/extras/wix.py index d87bfbb1..d87bfbb1 100644 --- a/extras/wix.py +++ b/waflib/extras/wix.py diff --git a/extras/xcode6.py b/waflib/extras/xcode6.py index 91bbff18..91bbff18 100644 --- a/extras/xcode6.py +++ b/waflib/extras/xcode6.py diff --git a/fixpy2.py b/waflib/fixpy2.py index 24176e06..24176e06 100644 --- a/fixpy2.py +++ b/waflib/fixpy2.py diff --git a/processor.py b/waflib/processor.py index 2eecf3bd..2eecf3bd 100755 --- a/processor.py +++ b/waflib/processor.py diff --git a/waflib/waf b/waflib/waf new file mode 100755 index 00000000..e22930a6 --- /dev/null +++ b/waflib/waf @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +# Minimal waf script for projects that include waflib directly + +from waflib import Context, Scripting + +import inspect +import os + +def main(): + script_path = os.path.abspath(inspect.getfile(inspect.getmodule(main))) + project_path = os.path.dirname(script_path) + Scripting.waf_entry_point(os.getcwd(), Context.WAFVERSION, project_path) + +if __name__ == '__main__': + main() diff --git a/wscript b/wscript new file mode 100644 index 00000000..363dc5f9 --- /dev/null +++ b/wscript @@ -0,0 +1,370 @@ +#!/usr/bin/env python +import os +import subprocess +import waflib.Logs as Logs +import waflib.Options as Options +import waflib.Utils as Utils +import waflib.extras.autowaf as autowaf + +# Package version +INGEN_VERSION = '0.5.1' + +# Mandatory waf variables +APPNAME = 'ingen' # Package name for waf dist +VERSION = INGEN_VERSION # Package version for waf dist +top = '.' # Source directory +out = 'build' # Build directory + +def options(ctx): + ctx.load('compiler_cxx') + ctx.load('python') + ctx.load('lv2') + ctx.recurse('src/gui') + autowaf.set_options(ctx, test=True) + opt = ctx.get_option_group('Configuration options') + opt.add_option('--data-dir', type='string', dest='datadir', + help='ingen data install directory [Default: PREFIX/share/ingen]') + opt.add_option('--module-dir', type='string', dest='moduledir', + help='ingen module install directory [Default: PREFIX/lib/ingen]') + opt.add_option('--no-gui', action='store_true', dest='no_gui', + help='do not build GUI') + opt.add_option('--no-client', action='store_true', dest='no_client', + help='do not build client library (or GUI)') + opt.add_option('--no-jack', action='store_true', dest='no_jack', + help='do not build jack backend (for ingen.lv2 only)') + opt.add_option('--no-plugin', action='store_true', dest='no_plugin', + help='do not build ingen.lv2 plugin') + opt.add_option('--no-python', action='store_true', dest='no_python', + help='do not install Python bindings') + opt.add_option('--no-webkit', action='store_true', dest='no_webkit', + help='do not use webkit to display plugin documentation') + opt.add_option('--no-jack-session', action='store_true', default=False, + dest='no_jack_session', + help='do not build JACK session support') + opt.add_option('--no-socket', action='store_true', dest='no_socket', + help='do not build Socket interface') + opt.add_option('--debug-urids', action='store_true', dest='debug_urids', + help='print a trace of URI mapping') + opt.add_option('--portaudio', action='store_true', default=False, + dest='portaudio', + help='build PortAudio backend') + +def configure(conf): + autowaf.display_header('Ingen Configuration') + autowaf.set_line_just(conf, 45) + conf.load('compiler_cxx', cache=True) + conf.load('lv2', cache=True) + if not Options.options.no_python: + conf.load('python', cache=True) + + conf.load('autowaf', cache=True) + autowaf.set_cxx_lang(conf, 'c++11') + + conf.check_cxx(header_name='boost/format.hpp') + conf.check_cxx(header_name='boost/intrusive/slist.hpp') + conf.check_cxx(header_name='boost/intrusive_ptr.hpp') + conf.check_cxx(header_name='boost/optional.hpp') + conf.check_cxx(header_name='boost/variant.hpp') + conf.check_cxx(msg='Checking for thread_local keyword', + mandatory=False, + fragment='thread_local int i = 0; int main() {}', + define_name='INGEN_HAVE_THREAD_LOCAL') + if not conf.is_defined('INGEN_HAVE_THREAD_LOCAL'): + conf.check_cxx(msg='Checking for __thread keyword', + mandatory=False, + fragment='__thread int i = 0; int main() {}', + define_name='INGEN_HAVE_THREAD_BUILTIN') + + autowaf.check_pkg(conf, 'lv2', uselib_store='LV2', + atleast_version='1.15.3', mandatory=True) + autowaf.check_pkg(conf, 'lilv-0', uselib_store='LILV', + atleast_version='0.21.5', mandatory=True) + autowaf.check_pkg(conf, 'suil-0', uselib_store='SUIL', + atleast_version='0.8.7', mandatory=True) + autowaf.check_pkg(conf, 'sratom-0', uselib_store='SRATOM', + atleast_version='0.4.6', mandatory=True) + autowaf.check_pkg(conf, 'raul', uselib_store='RAUL', + atleast_version='0.8.10', mandatory=True) + autowaf.check_pkg(conf, 'serd-0', uselib_store='SERD', + atleast_version='0.30.0', mandatory=False) + autowaf.check_pkg(conf, 'sord-0', uselib_store='SORD', + atleast_version='0.12.0', mandatory=False) + autowaf.check_pkg(conf, 'portaudio-2.0', uselib_store='PORTAUDIO', + atleast_version='2.0.0', mandatory=False) + + autowaf.check_function(conf, 'cxx', 'posix_memalign', + defines = '_POSIX_C_SOURCE=200809L', + header_name = 'stdlib.h', + define_name = 'HAVE_POSIX_MEMALIGN', + mandatory = False) + + autowaf.check_function(conf, 'cxx', 'isatty', + header_name = 'unistd.h', + defines = '_POSIX_C_SOURCE=200809L', + define_name = 'HAVE_ISATTY', + mandatory = False) + + autowaf.check_function(conf, 'cxx', 'vasprintf', + header_name = 'stdio.h', + defines = '_GNU_SOURCE=1', + define_name = 'HAVE_VASPRINTF', + mandatory = False) + + conf.check(define_name = 'HAVE_LIBDL', + lib = 'dl', + mandatory = False) + + if not Options.options.no_socket: + autowaf.check_function(conf, 'cxx', 'socket', + header_name = 'sys/socket.h', + define_name = 'HAVE_SOCKET', + mandatory = False) + + if not Options.options.no_python: + conf.check_python_version((2,4,0), mandatory=False) + + if not Options.options.no_plugin: + autowaf.define(conf, 'INGEN_BUILD_LV2', 1) + + if not Options.options.no_jack: + autowaf.check_pkg(conf, 'jack', uselib_store='JACK', + atleast_version='0.120.0', mandatory=False) + autowaf.check_function(conf, 'cxx', 'jack_set_property', + header_name = 'jack/metadata.h', + define_name = 'HAVE_JACK_METADATA', + uselib = 'JACK', + mandatory = False) + autowaf.check_function(conf, 'cxx', 'jack_port_rename', + header_name = 'jack/jack.h', + define_name = 'HAVE_JACK_PORT_RENAME', + uselib = 'JACK', + mandatory = False) + if not Options.options.no_jack_session: + autowaf.define(conf, 'INGEN_JACK_SESSION', 1) + + if Options.options.debug_urids: + autowaf.define(conf, 'INGEN_DEBUG_URIDS', 1) + + conf.env.INGEN_TEST_LINKFLAGS = [] + conf.env.INGEN_TEST_CXXFLAGS = [] + if conf.env.BUILD_TESTS: + if not conf.env.NO_COVERAGE: + conf.env.INGEN_TEST_CXXFLAGS += ['--coverage'] + conf.env.INGEN_TEST_LINKFLAGS += ['--coverage'] + + conf.env.PTHREAD_CFLAGS = [] + conf.env.PTHREAD_LINKFLAGS = [] + if conf.check(cflags=['-pthread'], mandatory=False): + conf.env.PTHREAD_CFLAGS = ['-pthread'] + if conf.check(linkflags=['-pthread'], mandatory=False): + if not (conf.env.DEST_OS == 'darwin' and conf.env.CXX_NAME == 'clang'): + conf.env.PTHREAD_LINKFLAGS += ['-pthread'] + if conf.check(linkflags=['-lpthread'], mandatory=False): + conf.env.PTHREAD_LINKFLAGS += ['-lpthread'] + + autowaf.define(conf, 'INGEN_SHARED', 1); + autowaf.define(conf, 'INGEN_VERSION', INGEN_VERSION) + + if not Options.options.no_client: + autowaf.define(conf, 'INGEN_BUILD_CLIENT', 1) + else: + Options.options.no_gui = True + + if not Options.options.no_gui: + conf.recurse('src/gui') + + if conf.env.HAVE_JACK: + autowaf.define(conf, 'HAVE_JACK_MIDI', 1) + + autowaf.define(conf, 'INGEN_DATA_DIR', + os.path.join(conf.env.DATADIR, 'ingen')) + autowaf.define(conf, 'INGEN_MODULE_DIR', + conf.env.LIBDIR) + autowaf.define(conf, 'INGEN_BUNDLE_DIR', + os.path.join(conf.env.LV2DIR, 'ingen.lv2')) + + conf.write_config_header('ingen_config.h', remove=False) + + autowaf.display_summary(conf) + autowaf.display_msg(conf, "GUI", bool(conf.env.INGEN_BUILD_GUI)) + autowaf.display_msg(conf, "HTML plugin documentation support", + bool(conf.env.HAVE_WEBKIT)) + autowaf.display_msg(conf, "PortAudio driver", bool(conf.env.HAVE_PORTAUDIO)) + autowaf.display_msg(conf, "Jack driver", bool(conf.env.HAVE_JACK)) + autowaf.display_msg(conf, "Jack session support", + bool(conf.env.INGEN_JACK_SESSION)) + autowaf.display_msg(conf, "Jack metadata support", + conf.is_defined('HAVE_JACK_METADATA')) + autowaf.display_msg(conf, "LV2 plugin driver", bool(conf.env.INGEN_BUILD_LV2)) + autowaf.display_msg(conf, "LV2 bundle", conf.env.INGEN_BUNDLE_DIR) + autowaf.display_msg(conf, "LV2 plugin support", bool(conf.env.HAVE_LILV)) + autowaf.display_msg(conf, "Socket interface", conf.is_defined('HAVE_SOCKET')) + print('') + +unit_tests = ['tst_FilePath'] + +def build(bld): + opts = Options.options + opts.datadir = opts.datadir or bld.env.PREFIX + 'share' + opts.moduledir = opts.moduledir or bld.env.PREFIX + 'lib/ingen' + + # Headers + for i in ['', 'client']: + bld.install_files('${INCLUDEDIR}/ingen/%s' % i, + bld.path.ant_glob('ingen/%s/*' % i)) + + # Python modules + if bld.env.PYTHONDIR: + bld.install_files('${PYTHONDIR}/', 'scripts/ingen.py') + + # Modules + bld.recurse('src') + bld.recurse('src/server') + + if bld.env.INGEN_BUILD_CLIENT: + bld.recurse('src/client') + + if bld.env.INGEN_BUILD_GUI: + bld.recurse('src/gui') + + # Program + obj = bld(features = 'c cxx cxxprogram', + source = 'src/ingen/ingen.cpp', + target = 'src/ingen/ingen', + includes = ['.'], + use = 'libingen', + install_path = '${BINDIR}') + autowaf.use_lib(bld, obj, 'GTHREAD GLIBMM SORD RAUL LILV INGEN LV2') + + # Test program + if bld.env.BUILD_TESTS: + for i in ['ingen_test', 'ingen_bench'] + unit_tests: + obj = bld(features = 'cxx cxxprogram', + source = 'tests/%s.cpp' % i, + target = 'tests/%s' % i, + includes = ['.'], + use = 'libingen', + install_path = '', + cxxflags = bld.env.INGEN_TEST_CXXFLAGS, + linkflags = bld.env.INGEN_TEST_LINKFLAGS) + autowaf.use_lib(bld, obj, 'GTHREAD GLIBMM SORD RAUL LILV INGEN LV2 SRATOM') + + bld.install_files('${DATADIR}/applications', 'src/ingen/ingen.desktop') + bld.install_files('${BINDIR}', 'scripts/ingenish', chmod=Utils.O755) + bld.install_files('${BINDIR}', 'scripts/ingenams', chmod=Utils.O755) + + # Code documentation + autowaf.build_dox(bld, 'INGEN', INGEN_VERSION, top, out) + + # Ontology documentation + if bld.env.DOCS: + bld(rule='lv2specgen.py ${SRC} ${TGT} -i -p ingen --copy-style --list-email ingen@drobilla.net --list-page http://lists.drobilla.net/listinfo.cgi/ingen-drobilla.net', + source = 'bundles/ingen.lv2/ingen.ttl', + target = 'ingen.lv2/ingen.html') + + # Man page + bld.install_files('${MANDIR}/man1', 'doc/ingen.1') + + # Icons + icon_dir = os.path.join(bld.env.DATADIR, 'icons', 'hicolor') + icon_sizes = [16, 22, 24, 32, 48, 64, 128, 256] + for s in icon_sizes: + d = '%dx%d' % (s, s) + bld.install_as( + os.path.join(icon_dir, d, 'apps', 'ingen.png'), + os.path.join('icons', d, 'ingen.png')) + + bld.install_as( + os.path.join(icon_dir, 'scalable', 'apps', 'ingen.svg'), + os.path.join('icons', 'scalable', 'ingen.svg')) + + bld.install_files('${LV2DIR}/ingen.lv2/', + bld.path.ant_glob('bundles/ingen.lv2/*')) + + # Install template graph bundles + for c in ['Stereo', 'Mono']: + for t in ['Effect', 'Instrument']: + bundle = '%s%s.ingen' % (c, t) + bld.install_files('${LV2DIR}/%s/' % bundle, + bld.path.ant_glob('bundles/%s/*' % bundle)) + + bld.add_post_fun(autowaf.run_ldconfig) + +def lint(ctx): + "checks code for style issues" + import subprocess + cmd = ("clang-tidy -p=. -header-filter=ingen/ -checks=\"*," + + "-clang-analyzer-alpha.*," + + "-cppcoreguidelines-*," + + "-cppcoreguidelines-pro-type-union-access," + + "-google-build-using-namespace," + + "-google-readability-casting," + + "-google-readability-todo," + + "-llvm-header-guard," + + "-llvm-include-order," + + "-llvm-namespace-comment," + + "-misc-unused-parameters," + + "-readability-else-after-return," + + "-readability-implicit-bool-cast," + + "-readability-named-parameter\" " + + "$(find .. -name '*.cpp')") + subprocess.call(cmd, cwd='build', shell=True) + +def upload_docs(ctx): + import shutil + + # Ontology documentation + os.system('rsync -avz -e ssh bundles/ingen.lv2/ingen.ttl drobilla@drobilla.net:~/drobilla.net/ns/') + os.system('rsync -avz -e ssh build/ingen.lv2/ingen.html drobilla@drobilla.net:~/drobilla.net/ns/') + os.system('rsync -avz -e ssh build/ingen.lv2/style.css drobilla@drobilla.net:~/drobilla.net/ns/') + + # Doxygen documentation + os.system('rsync -ravz --delete -e ssh build/doc/html/* drobilla@drobilla.net:~/drobilla.net/docs/ingen/') + + +def test(ctx): + import difflib + import sys + + def test_file_equals(path1, path2): + diff = list(difflib.unified_diff(open(path2).readlines(), + open(path1).readlines(), + path2, + path1)) + autowaf.run_test(ctx, APPNAME, [path2, len(diff) != 0]) + if len(diff) > 0: + for line in diff: + sys.stdout.write(line) + + os.environ['PATH'] = 'tests' + os.pathsep + os.getenv('PATH') + os.environ['LD_LIBRARY_PATH'] = os.path.join('src') + os.environ['INGEN_MODULE_PATH'] = os.pathsep.join([ + os.path.join('src', 'server')]) + + autowaf.pre_test(ctx, APPNAME, dirs=['.', 'src', 'tests']) + + with autowaf.begin_tests(ctx, APPNAME, 'unit'): + for i in unit_tests: + autowaf.run_test(ctx, APPNAME, 'tests/' + i) + + with autowaf.begin_tests(ctx, APPNAME, 'system'): + empty = ctx.path.find_node('tests/empty.ingen') + empty_path = os.path.join(empty.abspath(), 'main.ttl') + for i in ctx.path.ant_glob('tests/*.ttl'): + # Run test + autowaf.run_test(ctx, APPNAME, + 'ingen_test --load %s --execute %s' % (empty.abspath(), i.abspath()), + dirs=['.', 'src', 'tests']) + + # Check undo output for changes + base = os.path.basename(i.abspath().replace('.ttl', '')) + undone_path = base + '.undo.ingen/main.ttl' + test_file_equals(empty_path, os.path.abspath(undone_path)) + + # Check redo output for changes + out_path = base + '.out.ingen/main.ttl' + redone_path = base + '.redo.ingen/main.ttl' + test_file_equals(out_path, os.path.abspath(redone_path)) + + autowaf.post_test(ctx, APPNAME, dirs=['.', 'src', 'tests'], + remove=['/usr*']) |