aboutsummaryrefslogtreecommitdiffstats
path: root/plugins/eg-scope.lv2
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/eg-scope.lv2')
-rw-r--r--plugins/eg-scope.lv2/README.txt32
-rw-r--r--plugins/eg-scope.lv2/examploscope.c427
-rw-r--r--plugins/eg-scope.lv2/examploscope.ttl.in130
-rw-r--r--plugins/eg-scope.lv2/examploscope_ui.c676
-rw-r--r--plugins/eg-scope.lv2/manifest.ttl.in21
-rw-r--r--plugins/eg-scope.lv2/uris.h71
-rw-r--r--plugins/eg-scope.lv2/wscript70
7 files changed, 1427 insertions, 0 deletions
diff --git a/plugins/eg-scope.lv2/README.txt b/plugins/eg-scope.lv2/README.txt
new file mode 100644
index 0000000..122794c
--- /dev/null
+++ b/plugins/eg-scope.lv2/README.txt
@@ -0,0 +1,32 @@
+== Simple Oscilloscope ==
+
+This plugin displays the waveform of an incoming audio signal using a simple
+GTK+Cairo GUI.
+
+This plugin illustrates:
+
+- UI <==> Plugin communication via http://lv2plug.in/ns/ext/atom/[LV2 Atom] events
+- Atom vector usage and resize-port extension
+- Save/Restore UI state by communicating state to backend
+- Saving simple key/value state via the http://lv2plug.in/ns/ext/state/[LV2 State] extension
+- Cairo drawing and partial exposure
+
+This plugin intends to outline the basics for building visualization plugins
+that rely on atom communication. The UI looks like an oscilloscope, but is not
+a real oscilloscope implementation:
+
+- There is no display synchronisation, results will depend on LV2 host.
+- It displays raw audio samples, which a proper scope must not do.
+- The display itself just connects min/max line segments.
+- No triggering or synchronization.
+- No labels, no scale, no calibration, no markers, no numeric readout, etc.
+
+Addressing these issues is beyond the scope of this example.
+
+Please see http://lac.linuxaudio.org/2013/papers/36.pdf for scope design,
+https://wiki.xiph.org/Videos/Digital_Show_and_Tell for background information,
+and http://lists.lv2plug.in/pipermail/devel-lv2plug.in/2013-November/000545.html
+for general LV2 related conceptual criticism regarding real-time visualizations.
+
+A proper oscilloscope based on this example can be found at
+https://github.com/x42/sisco.lv2
diff --git a/plugins/eg-scope.lv2/examploscope.c b/plugins/eg-scope.lv2/examploscope.c
new file mode 100644
index 0000000..aa542c8
--- /dev/null
+++ b/plugins/eg-scope.lv2/examploscope.c
@@ -0,0 +1,427 @@
+/*
+ Copyright 2016 David Robillard <d@drobilla.net>
+ Copyright 2013 Robin Gareus <robin@gareus.org>
+
+ 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.
+*/
+
+#include "./uris.h"
+
+#include "lv2/atom/atom.h"
+#include "lv2/atom/forge.h"
+#include "lv2/atom/util.h"
+#include "lv2/core/lv2.h"
+#include "lv2/core/lv2_util.h"
+#include "lv2/log/log.h"
+#include "lv2/log/logger.h"
+#include "lv2/state/state.h"
+#include "lv2/urid/urid.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ ==== Private Plugin Instance Structure ====
+
+ In addition to the usual port buffers and features, this plugin stores the
+ state of the UI here, so it can be opened and closed without losing the
+ current settings. The UI state is communicated between the plugin and the
+ UI using atom messages via a sequence port, similarly to MIDI I/O.
+*/
+typedef struct {
+ // Port buffers
+ float* input[2];
+ float* output[2];
+ const LV2_Atom_Sequence* control;
+ LV2_Atom_Sequence* notify;
+
+ // Atom forge and URI mapping
+ LV2_URID_Map* map;
+ ScoLV2URIs uris;
+ LV2_Atom_Forge forge;
+ LV2_Atom_Forge_Frame frame;
+
+ // Log feature and convenience API
+ LV2_Log_Logger logger;
+
+ // Instantiation settings
+ uint32_t n_channels;
+ double rate;
+
+ // UI state
+ bool ui_active;
+ bool send_settings_to_ui;
+ float ui_amp;
+ uint32_t ui_spp;
+} EgScope;
+
+/** ==== Port Indices ==== */
+typedef enum {
+ SCO_CONTROL = 0, // Event input
+ SCO_NOTIFY = 1, // Event output
+ SCO_INPUT0 = 2, // Audio input 0
+ SCO_OUTPUT0 = 3, // Audio output 0
+ SCO_INPUT1 = 4, // Audio input 1 (stereo variant)
+ SCO_OUTPUT1 = 5, // Audio input 2 (stereo variant)
+} PortIndex;
+
+/** ==== Instantiate Method ==== */
+static LV2_Handle
+instantiate(const LV2_Descriptor* descriptor,
+ double rate,
+ const char* bundle_path,
+ const LV2_Feature* const* features)
+{
+ (void)descriptor; // Unused variable
+ (void)bundle_path; // Unused variable
+
+ // Allocate and initialise instance structure.
+ EgScope* self = (EgScope*)calloc(1, sizeof(EgScope));
+ if (!self) {
+ return NULL;
+ }
+
+ // Get host features
+ const char* missing = lv2_features_query(
+ features,
+ LV2_LOG__log, &self->logger.log, false,
+ LV2_URID__map, &self->map, true,
+ NULL);
+ lv2_log_logger_set_map(&self->logger, self->map);
+ if (missing) {
+ lv2_log_error(&self->logger, "Missing feature <%s>\n", missing);
+ free(self);
+ return NULL;
+ }
+
+ // Decide which variant to use depending on the plugin URI
+ if (!strcmp(descriptor->URI, SCO_URI "#Stereo")) {
+ self->n_channels = 2;
+ } else if (!strcmp(descriptor->URI, SCO_URI "#Mono")) {
+ self->n_channels = 1;
+ } else {
+ free(self);
+ return NULL;
+ }
+
+ // Initialise local variables
+ self->ui_active = false;
+ self->send_settings_to_ui = false;
+ self->rate = rate;
+
+ // Set default UI settings
+ self->ui_spp = 50;
+ self->ui_amp = 1.0;
+
+ // Map URIs and initialise forge/logger
+ map_sco_uris(self->map, &self->uris);
+ lv2_atom_forge_init(&self->forge, self->map);
+
+ return (LV2_Handle)self;
+}
+
+/** ==== Connect Port Method ==== */
+static void
+connect_port(LV2_Handle handle,
+ uint32_t port,
+ void* data)
+{
+ EgScope* self = (EgScope*)handle;
+
+ switch ((PortIndex)port) {
+ case SCO_CONTROL:
+ self->control = (const LV2_Atom_Sequence*)data;
+ break;
+ case SCO_NOTIFY:
+ self->notify = (LV2_Atom_Sequence*)data;
+ break;
+ case SCO_INPUT0:
+ self->input[0] = (float*)data;
+ break;
+ case SCO_OUTPUT0:
+ self->output[0] = (float*)data;
+ break;
+ case SCO_INPUT1:
+ self->input[1] = (float*)data;
+ break;
+ case SCO_OUTPUT1:
+ self->output[1] = (float*)data;
+ break;
+ }
+}
+
+/**
+ ==== Utility Function: `tx_rawaudio` ====
+
+ This function forges a message for sending a vector of raw data. The object
+ is a http://lv2plug.in/ns/ext/atom#Blank[Blank] with a few properties, like:
+ [source,n3]
+ --------
+ []
+ a sco:RawAudio ;
+ sco:channelID 0 ;
+ sco:audioData [ 0.0, 0.0, ... ] .
+ --------
+
+ where the value of the `sco:audioData` property, `[ 0.0, 0.0, ... ]`, is a
+ http://lv2plug.in/ns/ext/atom#Vector[Vector] of
+ http://lv2plug.in/ns/ext/atom#Float[Float].
+*/
+static void
+tx_rawaudio(LV2_Atom_Forge* forge,
+ ScoLV2URIs* uris,
+ const int32_t channel,
+ const size_t n_samples,
+ const float* data)
+{
+ LV2_Atom_Forge_Frame frame;
+
+ // Forge container object of type 'RawAudio'
+ lv2_atom_forge_frame_time(forge, 0);
+ lv2_atom_forge_object(forge, &frame, 0, uris->RawAudio);
+
+ // Add integer 'channelID' property
+ lv2_atom_forge_key(forge, uris->channelID);
+ lv2_atom_forge_int(forge, channel);
+
+ // Add vector of floats 'audioData' property
+ lv2_atom_forge_key(forge, uris->audioData);
+ lv2_atom_forge_vector(
+ forge, sizeof(float), uris->atom_Float, n_samples, data);
+
+ // Close off object
+ lv2_atom_forge_pop(forge, &frame);
+}
+
+/** ==== Run Method ==== */
+static void
+run(LV2_Handle handle, uint32_t n_samples)
+{
+ EgScope* self = (EgScope*)handle;
+
+ /* Ensure notify port buffer is large enough to hold all audio-samples and
+ configuration settings. A minimum size was requested in the .ttl file,
+ but check here just to be sure.
+
+ TODO: Explain these magic numbers.
+ */
+ const size_t size = (sizeof(float) * n_samples + 64) * self->n_channels;
+ const uint32_t space = self->notify->atom.size;
+ if (space < size + 128) {
+ /* Insufficient space, report error and do nothing. Note that a
+ real-time production plugin mustn't call log functions in run(), but
+ this can be useful for debugging and example purposes.
+ */
+ lv2_log_error(&self->logger, "Buffer size is insufficient\n");
+ return;
+ }
+
+ // Prepare forge buffer and initialize atom-sequence
+ lv2_atom_forge_set_buffer(&self->forge, (uint8_t*)self->notify, space);
+ lv2_atom_forge_sequence_head(&self->forge, &self->frame, 0);
+
+ /* Send settings to UI
+
+ The plugin can continue to run while the UI is closed and re-opened.
+ The state and settings of the UI are kept here and transmitted to the UI
+ every time it asks for them or if the user initializes a 'load preset'.
+ */
+ if (self->send_settings_to_ui && self->ui_active) {
+ self->send_settings_to_ui = false;
+ // Forge container object of type 'ui_state'
+ LV2_Atom_Forge_Frame frame;
+ lv2_atom_forge_frame_time(&self->forge, 0);
+ lv2_atom_forge_object(&self->forge, &frame, 0, self->uris.ui_State);
+
+ // Add UI state as properties
+ lv2_atom_forge_key(&self->forge, self->uris.ui_spp);
+ lv2_atom_forge_int(&self->forge, self->ui_spp);
+ lv2_atom_forge_key(&self->forge, self->uris.ui_amp);
+ lv2_atom_forge_float(&self->forge, self->ui_amp);
+ lv2_atom_forge_key(&self->forge, self->uris.param_sampleRate);
+ lv2_atom_forge_float(&self->forge, self->rate);
+ lv2_atom_forge_pop(&self->forge, &frame);
+ }
+
+ // Process incoming events from GUI
+ if (self->control) {
+ const LV2_Atom_Event* ev = lv2_atom_sequence_begin(
+ &(self->control)->body);
+ // For each incoming message...
+ while (!lv2_atom_sequence_is_end(
+ &self->control->body, self->control->atom.size, ev)) {
+ // If the event is an atom:Blank object
+ if (lv2_atom_forge_is_object_type(&self->forge, ev->body.type)) {
+ const LV2_Atom_Object* obj = (const LV2_Atom_Object*)&ev->body;
+ if (obj->body.otype == self->uris.ui_On) {
+ // If the object is a ui-on, the UI was activated
+ self->ui_active = true;
+ self->send_settings_to_ui = true;
+ } else if (obj->body.otype == self->uris.ui_Off) {
+ // If the object is a ui-off, the UI was closed
+ self->ui_active = false;
+ } else if (obj->body.otype == self->uris.ui_State) {
+ // If the object is a ui-state, it's the current UI settings
+ const LV2_Atom* spp = NULL;
+ const LV2_Atom* amp = NULL;
+ lv2_atom_object_get(obj, self->uris.ui_spp, &spp,
+ self->uris.ui_amp, &amp,
+ 0);
+ if (spp) {
+ self->ui_spp = ((const LV2_Atom_Int*)spp)->body;
+ }
+ if (amp) {
+ self->ui_amp = ((const LV2_Atom_Float*)amp)->body;
+ }
+ }
+ }
+ ev = lv2_atom_sequence_next(ev);
+ }
+ }
+
+ // Process audio data
+ for (uint32_t c = 0; c < self->n_channels; ++c) {
+ if (self->ui_active) {
+ // If UI is active, send raw audio data to UI
+ tx_rawaudio(&self->forge, &self->uris, c, n_samples, self->input[c]);
+ }
+ // If not processing audio in-place, forward audio
+ if (self->input[c] != self->output[c]) {
+ memcpy(self->output[c], self->input[c], sizeof(float) * n_samples);
+ }
+ }
+
+ // Close off sequence
+ lv2_atom_forge_pop(&self->forge, &self->frame);
+}
+
+static void
+cleanup(LV2_Handle handle)
+{
+ free(handle);
+}
+
+
+/**
+ ==== State Methods ====
+
+ This plugin's state consists of two basic properties: one `int` and one
+ `float`. No files are used. Note these values are POD, but not portable,
+ since different machines may have a different integer endianness or floating
+ point format. However, since standard Atom types are used, a good host will
+ be able to save them portably as text anyway.
+*/
+static LV2_State_Status
+state_save(LV2_Handle instance,
+ LV2_State_Store_Function store,
+ LV2_State_Handle handle,
+ uint32_t flags,
+ const LV2_Feature* const* features)
+{
+ EgScope* self = (EgScope*)instance;
+ if (!self) {
+ return LV2_STATE_SUCCESS;
+ }
+
+ store(handle, self->uris.ui_spp,
+ (void*)&self->ui_spp, sizeof(uint32_t),
+ self->uris.atom_Int,
+ LV2_STATE_IS_POD);
+
+ store(handle, self->uris.ui_amp,
+ (void*)&self->ui_amp, sizeof(float),
+ self->uris.atom_Float,
+ LV2_STATE_IS_POD);
+
+ return LV2_STATE_SUCCESS;
+}
+
+static LV2_State_Status
+state_restore(LV2_Handle instance,
+ LV2_State_Retrieve_Function retrieve,
+ LV2_State_Handle handle,
+ uint32_t flags,
+ const LV2_Feature* const* features)
+{
+ EgScope* self = (EgScope*)instance;
+
+ size_t size;
+ uint32_t type;
+ uint32_t valflags;
+
+ const void* spp = retrieve(
+ handle, self->uris.ui_spp, &size, &type, &valflags);
+ if (spp && size == sizeof(uint32_t) && type == self->uris.atom_Int) {
+ self->ui_spp = *((const uint32_t*)spp);
+ self->send_settings_to_ui = true;
+ }
+
+ const void* amp = retrieve(
+ handle, self->uris.ui_amp, &size, &type, &valflags);
+ if (amp && size == sizeof(float) && type == self->uris.atom_Float) {
+ self->ui_amp = *((const float*)amp);
+ self->send_settings_to_ui = true;
+ }
+
+ return LV2_STATE_SUCCESS;
+}
+
+static const void*
+extension_data(const char* uri)
+{
+ static const LV2_State_Interface state = { state_save, state_restore };
+ if (!strcmp(uri, LV2_STATE__interface)) {
+ return &state;
+ }
+ return NULL;
+}
+
+/** ==== Plugin Descriptors ==== */
+static const LV2_Descriptor descriptor_mono = {
+ SCO_URI "#Mono",
+ instantiate,
+ connect_port,
+ NULL,
+ run,
+ NULL,
+ cleanup,
+ extension_data
+};
+
+static const LV2_Descriptor descriptor_stereo = {
+ SCO_URI "#Stereo",
+ instantiate,
+ connect_port,
+ NULL,
+ run,
+ NULL,
+ cleanup,
+ extension_data
+};
+
+LV2_SYMBOL_EXPORT
+const LV2_Descriptor*
+lv2_descriptor(uint32_t index)
+{
+ switch (index) {
+ case 0:
+ return &descriptor_mono;
+ case 1:
+ return &descriptor_stereo;
+ default:
+ return NULL;
+ }
+}
diff --git a/plugins/eg-scope.lv2/examploscope.ttl.in b/plugins/eg-scope.lv2/examploscope.ttl.in
new file mode 100644
index 0000000..0b76962
--- /dev/null
+++ b/plugins/eg-scope.lv2/examploscope.ttl.in
@@ -0,0 +1,130 @@
+@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
+@prefix bufsz: <http://lv2plug.in/ns/ext/buf-size#> .
+@prefix doap: <http://usefulinc.com/ns/doap#> .
+@prefix foaf: <http://xmlns.com/foaf/0.1/> .
+@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix ui: <http://lv2plug.in/ns/extensions/ui#> .
+@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
+@prefix rsz: <http://lv2plug.in/ns/ext/resize-port#> .
+@prefix state: <http://lv2plug.in/ns/ext/state#> .
+@prefix egscope: <http://lv2plug.in/plugins/eg-scope#> .
+
+<http://gareus.org/rgareus#me>
+ a foaf:Person ;
+ foaf:name "Robin Gareus" ;
+ foaf:mbox <mailto:robin@gareus.org> ;
+ foaf:homepage <http://gareus.org/> .
+
+<http://lv2plug.in/plugins/eg-scope>
+ a doap:Project ;
+ doap:maintainer <http://gareus.org/rgareus#me> ;
+ doap:name "Example Scope" .
+
+egscope:Mono
+ a lv2:Plugin, lv2:AnalyserPlugin ;
+ doap:name "Example Scope (Mono)" ;
+ lv2:project <http://lv2plug.in/plugins/eg-scope> ;
+ doap:license <http://usefulinc.com/doap/licenses/gpl> ;
+ lv2:requiredFeature urid:map ;
+ lv2:optionalFeature lv2:hardRTCapable ;
+ lv2:extensionData state:interface ;
+ ui:ui egscope:ui ;
+ lv2:port [
+ a atom:AtomPort ,
+ lv2:InputPort ;
+ atom:bufferType atom:Sequence ;
+ lv2:designation lv2:control ;
+ lv2:index 0 ;
+ lv2:symbol "control" ;
+ lv2:name "Control"
+ ] , [
+ a atom:AtomPort ,
+ lv2:OutputPort ;
+ atom:bufferType atom:Sequence ;
+ lv2:designation lv2:control ;
+ lv2:index 1 ;
+ lv2:symbol "notify" ;
+ lv2:name "Notify" ;
+ # 8192 * sizeof(float) + LV2-Atoms
+ rsz:minimumSize 32832;
+ ] , [
+ a lv2:AudioPort ,
+ lv2:InputPort ;
+ lv2:index 2 ;
+ lv2:symbol "in" ;
+ lv2:name "In"
+ ] , [
+ a lv2:AudioPort ,
+ lv2:OutputPort ;
+ lv2:index 3 ;
+ lv2:symbol "out" ;
+ lv2:name "Out"
+ ] .
+
+
+egscope:Stereo
+ a lv2:Plugin, lv2:AnalyserPlugin ;
+ doap:name "Example Scope (Stereo)" ;
+ lv2:project <http://lv2plug.in/plugins/eg-scope> ;
+ doap:license <http://usefulinc.com/doap/licenses/gpl> ;
+ lv2:requiredFeature urid:map ;
+ lv2:optionalFeature lv2:hardRTCapable ;
+ lv2:extensionData state:interface ;
+ ui:ui egscope:ui ;
+ lv2:port [
+ a atom:AtomPort ,
+ lv2:InputPort ;
+ atom:bufferType atom:Sequence ;
+ lv2:designation lv2:control ;
+ lv2:index 0 ;
+ lv2:symbol "control" ;
+ lv2:name "Control"
+ ] , [
+ a atom:AtomPort ,
+ lv2:OutputPort ;
+ atom:bufferType atom:Sequence ;
+ lv2:designation lv2:control ;
+ lv2:index 1 ;
+ lv2:symbol "notify" ;
+ lv2:name "Notify" ;
+ rsz:minimumSize 65664;
+ ] , [
+ a lv2:AudioPort ,
+ lv2:InputPort ;
+ lv2:index 2 ;
+ lv2:symbol "in0" ;
+ lv2:name "InL"
+ ] , [
+ a lv2:AudioPort ,
+ lv2:OutputPort ;
+ lv2:index 3 ;
+ lv2:symbol "out0" ;
+ lv2:name "OutL"
+ ] , [
+ a lv2:AudioPort ,
+ lv2:InputPort ;
+ lv2:index 4 ;
+ lv2:symbol "in1" ;
+ lv2:name "InR"
+ ] , [
+ a lv2:AudioPort ,
+ lv2:OutputPort ;
+ lv2:index 5 ;
+ lv2:symbol "out1" ;
+ lv2:name "OutR"
+ ] .
+
+
+egscope:ui
+ a ui:GtkUI ;
+ lv2:requiredFeature urid:map ;
+ ui:portNotification [
+ ui:plugin egscope:Mono ;
+ lv2:symbol "notify" ;
+ ui:notifyType atom:Blank
+ ] , [
+ ui:plugin egscope:Stereo ;
+ lv2:symbol "notify" ;
+ ui:notifyType atom:Blank
+ ] .
diff --git a/plugins/eg-scope.lv2/examploscope_ui.c b/plugins/eg-scope.lv2/examploscope_ui.c
new file mode 100644
index 0000000..ce0000c
--- /dev/null
+++ b/plugins/eg-scope.lv2/examploscope_ui.c
@@ -0,0 +1,676 @@
+/*
+ Copyright 2013 Robin Gareus <robin@gareus.org>
+
+ 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.
+*/
+
+#include "./uris.h"
+
+#include "lv2/atom/atom.h"
+#include "lv2/atom/forge.h"
+#include "lv2/atom/util.h"
+#include "lv2/core/lv2.h"
+#include "lv2/ui/ui.h"
+#include "lv2/urid/urid.h"
+
+#include <cairo.h>
+#include <gdk/gdk.h>
+#include <glib-object.h>
+#include <glib.h>
+#include <gobject/gclosure.h>
+#include <gtk/gtk.h>
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+// Drawing area size
+#define DAWIDTH (640)
+#define DAHEIGHT (200)
+
+/**
+ Max continuous points on path. Many short-path segments are
+ expensive|inefficient long paths are not supported by all surfaces (usually
+ its a miter - not point - limit, depending on used cairo backend)
+*/
+#define MAX_CAIRO_PATH (128)
+
+/**
+ Representation of the raw audio-data for display (min | max) values for a
+ given 'index' position.
+*/
+typedef struct {
+ float data_min[DAWIDTH];
+ float data_max[DAWIDTH];
+
+ uint32_t idx;
+ uint32_t sub;
+} ScoChan;
+
+typedef struct {
+ LV2_Atom_Forge forge;
+ LV2_URID_Map* map;
+ ScoLV2URIs uris;
+
+ LV2UI_Write_Function write;
+ LV2UI_Controller controller;
+
+ GtkWidget* hbox;
+ GtkWidget* vbox;
+ GtkWidget* sep[2];
+ GtkWidget* darea;
+ GtkWidget* btn_pause;
+ GtkWidget* lbl_speed;
+ GtkWidget* lbl_amp;
+ GtkWidget* spb_speed;
+ GtkWidget* spb_amp;
+ GtkAdjustment* spb_speed_adj;
+ GtkAdjustment* spb_amp_adj;
+
+ ScoChan chn[2];
+ uint32_t stride;
+ uint32_t n_channels;
+ bool paused;
+ float rate;
+ bool updating;
+} EgScopeUI;
+
+
+/** Send current UI settings to backend. */
+static void
+send_ui_state(LV2UI_Handle handle)
+{
+ EgScopeUI* ui = (EgScopeUI*)handle;
+ const float gain = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_amp));
+
+ // Use local buffer on the stack to build atom
+ uint8_t obj_buf[1024];
+ lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf));
+
+ // Start a ui:State object
+ LV2_Atom_Forge_Frame frame;
+ LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object(
+ &ui->forge, &frame, 0, ui->uris.ui_State);
+
+ // msg[samples-per-pixel] = integer
+ lv2_atom_forge_key(&ui->forge, ui->uris.ui_spp);
+ lv2_atom_forge_int(&ui->forge, ui->stride);
+
+ // msg[amplitude] = float
+ lv2_atom_forge_key(&ui->forge, ui->uris.ui_amp);
+ lv2_atom_forge_float(&ui->forge, gain);
+
+ // Finish ui:State object
+ lv2_atom_forge_pop(&ui->forge, &frame);
+
+ // Send message to plugin port '0'
+ ui->write(ui->controller,
+ 0,
+ lv2_atom_total_size(msg),
+ ui->uris.atom_eventTransfer,
+ msg);
+}
+
+/** Notify backend that UI is closed. */
+static void
+send_ui_disable(LV2UI_Handle handle)
+{
+ EgScopeUI* ui = (EgScopeUI*)handle;
+ send_ui_state(handle);
+
+ uint8_t obj_buf[64];
+ lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf));
+
+ LV2_Atom_Forge_Frame frame;
+ LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object(
+ &ui->forge, &frame, 0, ui->uris.ui_Off);
+ lv2_atom_forge_pop(&ui->forge, &frame);
+ ui->write(ui->controller,
+ 0,
+ lv2_atom_total_size(msg),
+ ui->uris.atom_eventTransfer,
+ msg);
+}
+
+/**
+ Notify backend that UI is active.
+
+ The plugin should send state and enable data transmission.
+*/
+static void
+send_ui_enable(LV2UI_Handle handle)
+{
+ EgScopeUI* ui = (EgScopeUI*)handle;
+
+ uint8_t obj_buf[64];
+ lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf));
+
+ LV2_Atom_Forge_Frame frame;
+ LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object(
+ &ui->forge, &frame, 0, ui->uris.ui_On);
+ lv2_atom_forge_pop(&ui->forge, &frame);
+ ui->write(ui->controller,
+ 0,
+ lv2_atom_total_size(msg),
+ ui->uris.atom_eventTransfer,
+ msg);
+}
+
+/** Gtk widget callback. */
+static gboolean
+on_cfg_changed(GtkWidget* widget, gpointer data)
+{
+ EgScopeUI* ui = (EgScopeUI*)data;
+ if (!ui->updating) {
+ // Only send UI state if the change is from user interaction
+ send_ui_state(data);
+ }
+ return TRUE;
+}
+
+/**
+ Gdk drawing area draw callback.
+
+ Called in Gtk's main thread and uses Cairo to draw the data.
+*/
+static gboolean
+on_expose_event(GtkWidget* widget, GdkEventExpose* ev, gpointer data)
+{
+ EgScopeUI* ui = (EgScopeUI*)data;
+ const float gain = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_amp));
+
+ // Get cairo type for the gtk window
+ cairo_t* cr;
+ cr = gdk_cairo_create(ui->darea->window);
+
+ // Limit cairo-drawing to exposed area
+ cairo_rectangle(cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
+ cairo_clip(cr);
+
+ // Clear background
+ cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
+ cairo_rectangle(cr, 0, 0, DAWIDTH, DAHEIGHT * ui->n_channels);
+ cairo_fill(cr);
+
+ cairo_set_line_width(cr, 1.0);
+
+ const uint32_t start = ev->area.x;
+ const uint32_t end = ev->area.x + ev->area.width;
+
+ assert(start < DAWIDTH);
+ assert(end <= DAWIDTH);
+ assert(start < end);
+
+ for (uint32_t c = 0; c < ui->n_channels; ++c) {
+ ScoChan* chn = &ui->chn[c];
+
+ /* Drawing area Y-position of given sample-value.
+ * Note: cairo-pixel at 0 spans -0.5 .. +0.5, hence (DAHEIGHT / 2.0 -0.5)
+ * also the cairo Y-axis points upwards (hence 'minus value')
+ *
+ * == ( DAHEIGHT * (CHN) // channel offset
+ * + (DAHEIGHT / 2) - 0.5 // vertical center -- '0'
+ * - (DAHEIGHT / 2) * (VAL) * (GAIN)
+ * )
+ */
+ const float chn_y_offset = DAHEIGHT * c + DAHEIGHT * 0.5f - 0.5f;
+ const float chn_y_scale = DAHEIGHT * 0.5f * gain;
+
+#define CYPOS(VAL) (chn_y_offset - (VAL) * chn_y_scale)
+
+ cairo_save(cr);
+
+ /* Restrict drawing to current channel area, don't bleed drawing into
+ neighboring channels. */
+ cairo_rectangle(cr, 0, DAHEIGHT * c, DAWIDTH, DAHEIGHT);
+ cairo_clip(cr);
+
+ // Set color of wave-form
+ cairo_set_source_rgba(cr, 0.0, 1.0, 0.0, 1.0);
+
+ /* This is a somewhat 'smart' mechanism to plot audio data using
+ alternating up/down line-directions. It works well for both cases:
+ 1 pixel <= 1 sample and 1 pixel represents more than 1 sample, but
+ is not ideal for either. */
+ if (start == chn->idx) {
+ cairo_move_to(cr, start - 0.5, CYPOS(0));
+ } else {
+ cairo_move_to(cr, start - 0.5, CYPOS(chn->data_max[start]));
+ }
+
+ uint32_t pathlength = 0;
+ for (uint32_t i = start; i < end; ++i) {
+ if (i == chn->idx) {
+ continue;
+ } else if (i % 2) {
+ cairo_line_to(cr, i - .5, CYPOS(chn->data_min[i]));
+ cairo_line_to(cr, i - .5, CYPOS(chn->data_max[i]));
+ ++pathlength;
+ } else {
+ cairo_line_to(cr, i - .5, CYPOS(chn->data_max[i]));
+ cairo_line_to(cr, i - .5, CYPOS(chn->data_min[i]));
+ ++pathlength;
+ }
+
+ /** Limit the max cairo path length. This is an optimization trade
+ off: too short path: high load CPU/GPU load. too-long path:
+ bad anti-aliasing, or possibly lost points */
+ if (pathlength > MAX_CAIRO_PATH) {
+ pathlength = 0;
+ cairo_stroke(cr);
+ if (i % 2) {
+ cairo_move_to(cr, i - .5, CYPOS(chn->data_max[i]));
+ } else {
+ cairo_move_to(cr, i - .5, CYPOS(chn->data_min[i]));
+ }
+ }
+ }
+
+ if (pathlength > 0) {
+ cairo_stroke(cr);
+ }
+
+ // Draw current position vertical line if display is slow
+ if (ui->stride >= ui->rate / 4800.0f || ui->paused) {
+ cairo_set_source_rgba(cr, .9, .2, .2, .6);
+ cairo_move_to(cr, chn->idx - .5, DAHEIGHT * c);
+ cairo_line_to(cr, chn->idx - .5, DAHEIGHT * (c + 1));
+ cairo_stroke(cr);
+ }
+
+ // Undo the 'clipping' restriction
+ cairo_restore(cr);
+
+ // Channel separator
+ if (c > 0) {
+ cairo_set_source_rgba(cr, .5, .5, .5, 1.0);
+ cairo_move_to(cr, 0, DAHEIGHT * c - .5);
+ cairo_line_to(cr, DAWIDTH, DAHEIGHT * c - .5);
+ cairo_stroke(cr);
+ }
+
+ // Zero scale line
+ cairo_set_source_rgba(cr, .3, .3, .7, .5);
+ cairo_move_to(cr, 0, DAHEIGHT * (c + .5) - .5);
+ cairo_line_to(cr, DAWIDTH, DAHEIGHT * (c + .5) - .5);
+ cairo_stroke(cr);
+ }
+
+ cairo_destroy(cr);
+ return TRUE;
+}
+
+/**
+ Parse raw audio data and prepare for later drawing.
+
+ Note this is a toy example, which is really a waveform display, not an
+ oscilloscope. A serious scope would not display samples as is.
+
+ Signals above ~ 1/10 of the sampling-rate will not yield a useful visual
+ display and result in a rather unintuitive representation of the actual
+ waveform.
+
+ Ideally the audio-data would be buffered and upsampled here and after that
+ written in a display buffer for later use.
+
+ For more information, see
+ https://wiki.xiph.org/Videos/Digital_Show_and_Tell
+ http://lac.linuxaudio.org/2013/papers/36.pdf
+ and https://github.com/x42/sisco.lv2
+*/
+static int
+process_channel(EgScopeUI* ui,
+ ScoChan* chn,
+ const size_t n_elem,
+ float const* data,
+ uint32_t* idx_start,
+ uint32_t* idx_end)
+{
+ int overflow = 0;
+ *idx_start = chn->idx;
+ for (size_t i = 0; i < n_elem; ++i) {
+ if (data[i] < chn->data_min[chn->idx]) {
+ chn->data_min[chn->idx] = data[i];
+ }
+ if (data[i] > chn->data_max[chn->idx]) {
+ chn->data_max[chn->idx] = data[i];
+ }
+ if (++chn->sub >= ui->stride) {
+ chn->sub = 0;
+ chn->idx = (chn->idx + 1) % DAWIDTH;
+ if (chn->idx == 0) {
+ ++overflow;
+ }
+ chn->data_min[chn->idx] = 1.0;
+ chn->data_max[chn->idx] = -1.0;
+ }
+ }
+ *idx_end = chn->idx;
+ return overflow;
+}
+
+/**
+ Called via port_event() which is called by the host, typically at a rate of
+ around 25 FPS.
+*/
+static void
+update_scope(EgScopeUI* ui,
+ const int32_t channel,
+ const size_t n_elem,
+ float const* data)
+{
+ // Never trust input data which could lead to application failure.
+ if (channel < 0 || (uint32_t)channel > ui->n_channels) {
+ return;
+ }
+
+ // Update state in sync with 1st channel
+ if (channel == 0) {
+ ui->stride = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_speed));
+ const bool paused = gtk_toggle_button_get_active(
+ GTK_TOGGLE_BUTTON(ui->btn_pause));
+
+ if (paused != ui->paused) {
+ ui->paused = paused;
+ gtk_widget_queue_draw(ui->darea);
+ }
+ }
+ if (ui->paused) {
+ return;
+ }
+
+ uint32_t idx_start; // Display pixel start
+ uint32_t idx_end; // Display pixel end
+ int overflow; // Received more audio-data than display-pixel
+
+ // Process this channel's audio-data for display
+ ScoChan* chn = &ui->chn[channel];
+ overflow = process_channel(ui, chn, n_elem, data, &idx_start, &idx_end);
+
+ // Signal gtk's main thread to redraw the widget after the last channel
+ if ((uint32_t)channel + 1 == ui->n_channels) {
+ if (overflow > 1) {
+ // Redraw complete widget
+ gtk_widget_queue_draw(ui->darea);
+ } else if (idx_end > idx_start) {
+ // Redraw area between start -> end pixel
+ gtk_widget_queue_draw_area(ui->darea, idx_start - 2, 0, 3
+ + idx_end - idx_start,
+ DAHEIGHT * ui->n_channels);
+ } else if (idx_end < idx_start) {
+ // Wrap-around: redraw area between 0->start AND end->right-end
+ gtk_widget_queue_draw_area(
+ ui->darea,
+ idx_start - 2, 0,
+ 3 + DAWIDTH - idx_start, DAHEIGHT * ui->n_channels);
+ gtk_widget_queue_draw_area(
+ ui->darea,
+ 0, 0,
+ idx_end + 1, DAHEIGHT * ui->n_channels);
+ }
+ }
+}
+
+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)
+{
+ EgScopeUI* ui = (EgScopeUI*)calloc(1, sizeof(EgScopeUI));
+
+ if (!ui) {
+ fprintf(stderr, "EgScope.lv2 UI: out of memory\n");
+ return NULL;
+ }
+
+ ui->map = NULL;
+ *widget = NULL;
+
+ if (!strcmp(plugin_uri, SCO_URI "#Mono")) {
+ ui->n_channels = 1;
+ } else if (!strcmp(plugin_uri, SCO_URI "#Stereo")) {
+ ui->n_channels = 2;
+ } else {
+ free(ui);
+ return NULL;
+ }
+
+ for (int i = 0; features[i]; ++i) {
+ if (!strcmp(features[i]->URI, LV2_URID_URI "#map")) {
+ ui->map = (LV2_URID_Map*)features[i]->data;
+ }
+ }
+
+ if (!ui->map) {
+ fprintf(stderr, "EgScope.lv2 UI: Host does not support urid:map\n");
+ free(ui);
+ return NULL;
+ }
+
+ // Initialize private data structure
+ ui->write = write_function;
+ ui->controller = controller;
+
+ ui->vbox = NULL;
+ ui->hbox = NULL;
+ ui->darea = NULL;
+ ui->stride = 25;
+ ui->paused = false;
+ ui->rate = 48000;
+
+ ui->chn[0].idx = 0;
+ ui->chn[0].sub = 0;
+ ui->chn[1].idx = 0;
+ ui->chn[1].sub = 0;
+ memset(ui->chn[0].data_min, 0, sizeof(float) * DAWIDTH);
+ memset(ui->chn[0].data_max, 0, sizeof(float) * DAWIDTH);
+ memset(ui->chn[1].data_min, 0, sizeof(float) * DAWIDTH);
+ memset(ui->chn[1].data_max, 0, sizeof(float) * DAWIDTH);
+
+ map_sco_uris(ui->map, &ui->uris);
+ lv2_atom_forge_init(&ui->forge, ui->map);
+
+ // Setup UI
+ ui->hbox = gtk_hbox_new(FALSE, 0);
+ ui->vbox = gtk_vbox_new(FALSE, 0);
+
+ ui->darea = gtk_drawing_area_new();
+ gtk_widget_set_size_request(ui->darea, DAWIDTH, DAHEIGHT * ui->n_channels);
+
+ ui->lbl_speed = gtk_label_new("Samples/Pixel");
+ ui->lbl_amp = gtk_label_new("Amplitude");
+
+ ui->sep[0] = gtk_hseparator_new();
+ ui->sep[1] = gtk_label_new("");
+ ui->btn_pause = gtk_toggle_button_new_with_label("Pause");
+
+ ui->spb_speed_adj = (GtkAdjustment*)gtk_adjustment_new(
+ 25.0, 1.0, 1000.0, 1.0, 5.0, 0.0);
+ ui->spb_speed = gtk_spin_button_new(ui->spb_speed_adj, 1.0, 0);
+
+ ui->spb_amp_adj = (GtkAdjustment*)gtk_adjustment_new(
+ 1.0, 0.1, 6.0, 0.1, 1.0, 0.0);
+ ui->spb_amp = gtk_spin_button_new(ui->spb_amp_adj, 0.1, 1);
+
+ gtk_box_pack_start(GTK_BOX(ui->hbox), ui->darea, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(ui->hbox), ui->vbox, FALSE, FALSE, 4);
+
+ gtk_box_pack_start(GTK_BOX(ui->vbox), ui->lbl_speed, FALSE, FALSE, 2);
+ gtk_box_pack_start(GTK_BOX(ui->vbox), ui->spb_speed, FALSE, FALSE, 2);
+ gtk_box_pack_start(GTK_BOX(ui->vbox), ui->sep[0], FALSE, FALSE, 8);
+ gtk_box_pack_start(GTK_BOX(ui->vbox), ui->lbl_amp, FALSE, FALSE, 2);
+ gtk_box_pack_start(GTK_BOX(ui->vbox), ui->spb_amp, FALSE, FALSE, 2);
+ gtk_box_pack_start(GTK_BOX(ui->vbox), ui->sep[1], TRUE, FALSE, 8);
+ gtk_box_pack_start(GTK_BOX(ui->vbox), ui->btn_pause, FALSE, FALSE, 2);
+
+ g_signal_connect(G_OBJECT(ui->darea), "expose_event",
+ G_CALLBACK(on_expose_event), ui);
+ g_signal_connect(G_OBJECT(ui->spb_amp), "value-changed",
+ G_CALLBACK(on_cfg_changed), ui);
+ g_signal_connect(G_OBJECT(ui->spb_speed), "value-changed",
+ G_CALLBACK(on_cfg_changed), ui);
+
+ *widget = ui->hbox;
+
+ /* Send UIOn message to plugin, which will request state and enable message
+ transmission. */
+ send_ui_enable(ui);
+
+ return ui;
+}
+
+static void
+cleanup(LV2UI_Handle handle)
+{
+ EgScopeUI* ui = (EgScopeUI*)handle;
+ /* Send UIOff message to plugin, which will save state and disable message
+ * transmission. */
+ send_ui_disable(ui);
+ gtk_widget_destroy(ui->darea);
+ free(ui);
+}
+
+static int
+recv_raw_audio(EgScopeUI* ui, const LV2_Atom_Object* obj)
+{
+ const LV2_Atom* chan_val = NULL;
+ const LV2_Atom* data_val = NULL;
+ const int n_props = lv2_atom_object_get(
+ obj,
+ ui->uris.channelID, &chan_val,
+ ui->uris.audioData, &data_val,
+ NULL);
+
+ if (n_props != 2 ||
+ chan_val->type != ui->uris.atom_Int ||
+ data_val->type != ui->uris.atom_Vector) {
+ // Object does not have the required properties with correct types
+ fprintf(stderr, "eg-scope.lv2 UI error: Corrupt audio message\n");
+ return 1;
+ }
+
+ // Get the values we need from the body of the property value atoms
+ const int32_t chn = ((const LV2_Atom_Int*)chan_val)->body;
+ const LV2_Atom_Vector* vec = (const LV2_Atom_Vector*)data_val;
+ if (vec->body.child_type != ui->uris.atom_Float) {
+ return 1; // Vector has incorrect element type
+ }
+
+ // Number of elements = (total size - header size) / element size
+ const size_t n_elem = ((data_val->size - sizeof(LV2_Atom_Vector_Body))
+ / sizeof(float));
+
+ // Float elements immediately follow the vector body header
+ const float* data = (const float*)(&vec->body + 1);
+
+ // Update display
+ update_scope(ui, chn, n_elem, data);
+ return 0;
+}
+
+static int
+recv_ui_state(EgScopeUI* ui, const LV2_Atom_Object* obj)
+{
+ const LV2_Atom* spp_val = NULL;
+ const LV2_Atom* amp_val = NULL;
+ const LV2_Atom* rate_val = NULL;
+ const int n_props = lv2_atom_object_get(
+ obj,
+ ui->uris.ui_spp, &spp_val,
+ ui->uris.ui_amp, &amp_val,
+ ui->uris.param_sampleRate, &rate_val,
+ NULL);
+
+ if (n_props != 3 ||
+ spp_val->type != ui->uris.atom_Int ||
+ amp_val->type != ui->uris.atom_Float ||
+ rate_val->type != ui->uris.atom_Float) {
+ // Object does not have the required properties with correct types
+ fprintf(stderr, "eg-scope.lv2 UI error: Corrupt state message\n");
+ return 1;
+ }
+
+ // Get the values we need from the body of the property value atoms
+ const int32_t spp = ((const LV2_Atom_Int*)spp_val)->body;
+ const float amp = ((const LV2_Atom_Float*)amp_val)->body;
+ const float rate = ((const LV2_Atom_Float*)rate_val)->body;
+
+ // Disable transmission and update UI
+ ui->updating = true;
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->spb_speed), spp);
+ gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->spb_amp), amp);
+ ui->updating = false;
+ ui->rate = rate;
+
+ return 0;
+}
+
+/**
+ Receive data from the DSP-backend.
+
+ This is called by the host, typically at a rate of around 25 FPS.
+
+ Ideally this happens regularly and with relatively low latency, but there
+ are no hard guarantees about message delivery.
+*/
+static void
+port_event(LV2UI_Handle handle,
+ uint32_t port_index,
+ uint32_t buffer_size,
+ uint32_t format,
+ const void* buffer)
+{
+ EgScopeUI* ui = (EgScopeUI*)handle;
+ const LV2_Atom* atom = (const LV2_Atom*)buffer;
+
+ /* Check type of data received
+ * - format == 0: Control port event (float)
+ * - format > 0: Message (atom)
+ */
+ if (format == ui->uris.atom_eventTransfer &&
+ lv2_atom_forge_is_object_type(&ui->forge, atom->type)) {
+ const LV2_Atom_Object* obj = (const LV2_Atom_Object*)atom;
+ if (obj->body.otype == ui->uris.RawAudio) {
+ recv_raw_audio(ui, obj);
+ } else if (obj->body.otype == ui->uris.ui_State) {
+ recv_ui_state(ui, obj);
+ }
+ }
+}
+
+static const LV2UI_Descriptor descriptor = {
+ SCO_URI "#ui",
+ instantiate,
+ cleanup,
+ port_event,
+ NULL
+};
+
+LV2_SYMBOL_EXPORT
+const LV2UI_Descriptor*
+lv2ui_descriptor(uint32_t index)
+{
+ switch (index) {
+ case 0:
+ return &descriptor;
+ default:
+ return NULL;
+ }
+}
diff --git a/plugins/eg-scope.lv2/manifest.ttl.in b/plugins/eg-scope.lv2/manifest.ttl.in
new file mode 100644
index 0000000..a64aff1
--- /dev/null
+++ b/plugins/eg-scope.lv2/manifest.ttl.in
@@ -0,0 +1,21 @@
+@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix ui: <http://lv2plug.in/ns/extensions/ui#> .
+
+# ==== Mono plugin variant ====
+<http://lv2plug.in/plugins/eg-scope#Mono>
+ a lv2:Plugin ;
+ lv2:binary <examploscope@LIB_EXT@> ;
+ rdfs:seeAlso <examploscope.ttl> .
+
+# ==== Stereo plugin variant ====
+<http://lv2plug.in/plugins/eg-scope#Stereo>
+ a lv2:Plugin ;
+ lv2:binary <examploscope@LIB_EXT@> ;
+ rdfs:seeAlso <examploscope.ttl> .
+
+# ==== Gtk 2.0 UI ====
+<http://lv2plug.in/plugins/eg-scope#ui>
+ a ui:GtkUI ;
+ ui:binary <examploscope_ui@LIB_EXT@> ;
+ rdfs:seeAlso <examploscope.ttl> .
diff --git a/plugins/eg-scope.lv2/uris.h b/plugins/eg-scope.lv2/uris.h
new file mode 100644
index 0000000..2ebaf4e
--- /dev/null
+++ b/plugins/eg-scope.lv2/uris.h
@@ -0,0 +1,71 @@
+/*
+ Copyright 2013 Robin Gareus <robin@gareus.org>
+
+ 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.
+*/
+
+#ifndef SCO_URIS_H
+#define SCO_URIS_H
+
+#include "lv2/atom/atom.h"
+#include "lv2/atom/forge.h"
+#include "lv2/parameters/parameters.h"
+#include "lv2/urid/urid.h"
+
+#define SCO_URI "http://lv2plug.in/plugins/eg-scope"
+
+typedef struct {
+ // URIs defined in LV2 specifications
+ LV2_URID atom_Vector;
+ LV2_URID atom_Float;
+ LV2_URID atom_Int;
+ LV2_URID atom_eventTransfer;
+ LV2_URID param_sampleRate;
+
+ /* URIs defined for this plugin. It is best to re-use existing URIs as
+ much as possible, but plugins may need more vocabulary specific to their
+ needs. These are used as types and properties for plugin:UI
+ communication, as well as for saving state. */
+ LV2_URID RawAudio;
+ LV2_URID channelID;
+ LV2_URID audioData;
+ LV2_URID ui_On;
+ LV2_URID ui_Off;
+ LV2_URID ui_State;
+ LV2_URID ui_spp;
+ LV2_URID ui_amp;
+} ScoLV2URIs;
+
+static inline void
+map_sco_uris(LV2_URID_Map* map, ScoLV2URIs* uris)
+{
+ uris->atom_Vector = map->map(map->handle, LV2_ATOM__Vector);
+ uris->atom_Float = map->map(map->handle, LV2_ATOM__Float);
+ uris->atom_Int = map->map(map->handle, LV2_ATOM__Int);
+ uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer);
+ uris->param_sampleRate = map->map(map->handle, LV2_PARAMETERS__sampleRate);
+
+ /* Note the convention that URIs for types are capitalized, and URIs for
+ everything else (mainly properties) are not, just as in LV2
+ specifications. */
+ uris->RawAudio = map->map(map->handle, SCO_URI "#RawAudio");
+ uris->audioData = map->map(map->handle, SCO_URI "#audioData");
+ uris->channelID = map->map(map->handle, SCO_URI "#channelID");
+ uris->ui_On = map->map(map->handle, SCO_URI "#UIOn");
+ uris->ui_Off = map->map(map->handle, SCO_URI "#UIOff");
+ uris->ui_State = map->map(map->handle, SCO_URI "#UIState");
+ uris->ui_spp = map->map(map->handle, SCO_URI "#ui-spp");
+ uris->ui_amp = map->map(map->handle, SCO_URI "#ui-amp");
+}
+
+#endif /* SCO_URIS_H */
diff --git a/plugins/eg-scope.lv2/wscript b/plugins/eg-scope.lv2/wscript
new file mode 100644
index 0000000..76fece7
--- /dev/null
+++ b/plugins/eg-scope.lv2/wscript
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+from waflib.extras import autowaf as autowaf
+import re
+
+# Variables for 'waf dist'
+APPNAME = 'eg-scope.lv2'
+VERSION = '1.0.0'
+
+# Mandatory variables
+top = '.'
+out = 'build'
+
+def options(opt):
+ opt.load('compiler_c')
+ opt.load('lv2')
+ autowaf.set_options(opt)
+
+def configure(conf):
+ conf.load('compiler_c', cache=True)
+ conf.load('lv2', cache=True)
+ conf.load('autowaf', cache=True)
+
+ if not autowaf.is_child():
+ autowaf.check_pkg(conf, 'lv2', atleast_version='1.2.1', uselib_store='LV2')
+
+ autowaf.check_pkg(conf, 'cairo', uselib_store='CAIRO',
+ atleast_version='1.8.10', mandatory=True)
+ autowaf.check_pkg(conf, 'gtk+-2.0', uselib_store='GTK2',
+ atleast_version='2.18.0', mandatory=False)
+
+def build(bld):
+ bundle = 'eg-scope.lv2'
+
+ # Make a pattern for shared objects without the 'lib' prefix
+ module_pat = re.sub('^lib', '', bld.env.cshlib_PATTERN)
+ module_ext = module_pat[module_pat.rfind('.'):]
+
+ # Build manifest.ttl by substitution (for portable lib extension)
+ for i in ['manifest.ttl', 'examploscope.ttl']:
+ bld(features = 'subst',
+ source = i + '.in',
+ target = '%s/%s' % (bundle, i),
+ install_path = '${LV2DIR}/%s' % bundle,
+ LIB_EXT = module_ext)
+
+ # Use LV2 headers from parent directory if building as a sub-project
+ includes = ['.']
+ if autowaf.is_child:
+ includes += ['../..']
+
+ # Build plugin library
+ obj = bld(features = 'c cshlib',
+ source = 'examploscope.c',
+ name = 'examploscope',
+ target = '%s/examploscope' % bundle,
+ install_path = '${LV2DIR}/%s' % bundle,
+ use = 'LV2',
+ includes = includes)
+ obj.env.cshlib_PATTERN = module_pat
+
+ # Build UI library
+ if bld.env.HAVE_GTK2:
+ obj = bld(features = 'c cshlib',
+ source = 'examploscope_ui.c',
+ name = 'examploscope_ui',
+ target = '%s/examploscope_ui' % bundle,
+ install_path = '${LV2DIR}/%s' % bundle,
+ use = 'GTK2 CAIRO LV2',
+ includes = includes)
+ obj.env.cshlib_PATTERN = module_pat