diff options
-rw-r--r-- | plugins/eg05-scope.lv2/README.txt | 30 | ||||
-rw-r--r-- | plugins/eg05-scope.lv2/examploscope.c | 404 | ||||
-rw-r--r-- | plugins/eg05-scope.lv2/examploscope.ttl.in | 130 | ||||
-rw-r--r-- | plugins/eg05-scope.lv2/examploscope_ui.c | 653 | ||||
-rw-r--r-- | plugins/eg05-scope.lv2/manifest.ttl.in | 18 | ||||
-rw-r--r-- | plugins/eg05-scope.lv2/uris.h | 73 | ||||
-rw-r--r-- | plugins/eg05-scope.lv2/wscript | 73 |
7 files changed, 1381 insertions, 0 deletions
diff --git a/plugins/eg05-scope.lv2/README.txt b/plugins/eg05-scope.lv2/README.txt new file mode 100644 index 0000000..ec3578b --- /dev/null +++ b/plugins/eg05-scope.lv2/README.txt @@ -0,0 +1,30 @@ +== Simple Oscilloscope == + +This plugin displays the wave-form of an incoming audio-signal using a simple +GTK+Cairo GUI. + +This plugin illustrates: + +- UI <==> Plugin communication via LV2 atom events +- LV2 Atom vector usage and resize-port extension +- Save/Restore UI state by communicating state to backend +- Cairo drawing and partial exposure + +This plugin intends to outline the basics for building visualization plugins +that rely on Atom communication. The UI looks likean 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/eg05-scope.lv2/examploscope.c b/plugins/eg05-scope.lv2/examploscope.c new file mode 100644 index 0000000..13281ab --- /dev/null +++ b/plugins/eg05-scope.lv2/examploscope.c @@ -0,0 +1,404 @@ +/* + 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 <stdio.h> +#include <stdlib.h> +#include <stdint.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/state/state.h" +#include "lv2/lv2plug.in/ns/lv2core/lv2.h" + +#include "./uris.h" + +/** Private plugin instance structure. */ +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_Log* log; + LV2_Log_Logger logger; + + // Instantiation settings + uint32_t n_channels; + double rate; + + /* The state of the UI is stored here, so that the GUI can be displayed and + closed without losing the current settings. It is communicated to the + UI using atom messages. + */ + bool ui_active; + bool send_settings_to_ui; + float ui_amp; + uint32_t ui_spp; +} EgScope; + +/** Port indices. */ +typedef enum { + SCO_CONTROL = 0, + SCO_NOTIFY = 1, + SCO_INPUT0 = 2, + SCO_OUTPUT0 = 3, + SCO_INPUT1 = 4, + SCO_OUTPUT1 = 5, +} PortIndex; + +/** Create plugin instance. */ +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 + for (int i = 0; features[i]; ++i) { + if (!strcmp(features[i]->URI, LV2_URID__map)) { + self->map = (LV2_URID_Map*)features[i]->data; + } else if (!strcmp(features[i]->URI, LV2_LOG__log)) { + self->log = (LV2_Log_Log*)features[i]->data; + } + } + + if (!self->map) { + fprintf(stderr, "EgScope.lv2 error: Host does not support urid:map\n"); + 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); + lv2_log_logger_init(&self->logger, self->map, self->log); + + return (LV2_Handle)self; +} + +/** Connect a port to a buffer. */ +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; + } +} + +/** + Forge vector of raw data. + + @param forge Forge to use. + @param uris Mapped URI identifiers. + @param channel Channel ID to transmit. + @param n_samples Number of audio samples to transmit. + @param data Actual audio data. +*/ +static void +tx_rawaudio(LV2_Atom_Forge* forge, + ScoLV2URIs* uris, + const int32_t channel, + const size_t n_samples, + void* data) +{ + LV2_Atom_Forge_Frame frame; + + // Forge container object of type 'rawaudio' + lv2_atom_forge_frame_time(forge, 0); + lv2_atom_forge_blank(forge, &frame, 1, uris->RawAudio); + + // Add integer attribute 'channelid' + lv2_atom_forge_property_head(forge, uris->channelID, 0); + lv2_atom_forge_int(forge, channel); + + // Add vector of floats raw 'audiodata' + lv2_atom_forge_property_head(forge, uris->audioData, 0); + lv2_atom_forge_vector( + forge, sizeof(float), uris->atom_Float, n_samples, data); + + // Close off atom-object + lv2_atom_forge_pop(forge, &frame); +} + +/** Process a block of audio */ +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_blank(&self->forge, &frame, 1, self->uris.ui_State); + + // Add UI state as properties + lv2_atom_forge_property_head(&self->forge, self->uris.ui_spp, 0); + lv2_atom_forge_int(&self->forge, self->ui_spp); + lv2_atom_forge_property_head(&self->forge, self->uris.ui_amp, 0); + lv2_atom_forge_float(&self->forge, self->ui_amp); + lv2_atom_forge_property_head(&self->forge, self->uris.param_sampleRate, 0); + lv2_atom_forge_float(&self->forge, self->rate); + lv2_atom_forge_pop(&self->forge, &frame); + } + + // Process incoming events from GUI + if (self->control) { + 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 (ev->body.type == self->uris.atom_Blank) { + const LV2_Atom_Object* obj = (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, &, + 0); + if (spp) { + self->ui_spp = ((LV2_Atom_Int*)spp)->body; + } + if (amp) { + self->ui_amp = ((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); +} + +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 state values. 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. */ + + 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 = *((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 = *((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; +} + +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/eg05-scope.lv2/examploscope.ttl.in b/plugins/eg05-scope.lv2/examploscope.ttl.in new file mode 100644 index 0000000..0b76962 --- /dev/null +++ b/plugins/eg05-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/eg05-scope.lv2/examploscope_ui.c b/plugins/eg05-scope.lv2/examploscope_ui.c new file mode 100644 index 0000000..ffbe573 --- /dev/null +++ b/plugins/eg05-scope.lv2/examploscope_ui.c @@ -0,0 +1,653 @@ +/* + 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 <stdio.h> +#include <stdlib.h> + +#include <cairo.h> +#include <gtk/gtk.h> + +#include "lv2/lv2plug.in/ns/extensions/ui/ui.h" +#include "./uris.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; +} 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)); + + // Event body is a ui_state object + LV2_Atom_Forge_Frame frame; + LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_blank( + &ui->forge, &frame, 1, ui->uris.ui_State); + + // msg[samples-per-pixel] = integer + lv2_atom_forge_property_head(&ui->forge, ui->uris.ui_spp, 0); + lv2_atom_forge_int(&ui->forge, ui->stride); + // msg[amplitude] = float + lv2_atom_forge_property_head(&ui->forge, ui->uris.ui_amp, 0); + lv2_atom_forge_float(&ui->forge, gain); + // Close off forged data + 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_blank( + &ui->forge, &frame, 1, 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_blank( + &ui->forge, &frame, 1, 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) +{ + 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*)malloc(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, LV2_Atom_Object* obj) +{ + LV2_Atom* chan_val = NULL; + 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 = ((LV2_Atom_Int*)chan_val)->body; + LV2_Atom_Vector* vec = (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)) + / sizeof(float)); + + // Float elements immediately follow the vector body header + const float* data = (float*)(&vec->body + 1); + + // Update display + update_scope(ui, chn, n_elem, data); + return 0; +} + +static int +recv_ui_state(EgScopeUI* ui, LV2_Atom_Object* obj) +{ + LV2_Atom* spp_val = NULL; + LV2_Atom* amp_val = NULL; + LV2_Atom* rate_val = NULL; + const int n_props = lv2_atom_object_get( + obj, + ui->uris.ui_spp, &spp_val, + ui->uris.ui_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 = ((LV2_Atom_Int*)spp_val)->body; + const float amp = ((LV2_Atom_Float*)amp_val)->body; + const float rate = ((LV2_Atom_Float*)rate_val)->body; + + // Update UI + 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->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; + LV2_Atom* atom = (LV2_Atom*)buffer; + + /* Check type of data received + * - format == 0: Control port event (float) + * - format > 0: Message (atom) + */ + if (format == ui->uris.atom_eventTransfer && + atom->type == ui->uris.atom_Blank) { + LV2_Atom_Object* obj = (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/eg05-scope.lv2/manifest.ttl.in b/plugins/eg05-scope.lv2/manifest.ttl.in new file mode 100644 index 0000000..028a673 --- /dev/null +++ b/plugins/eg05-scope.lv2/manifest.ttl.in @@ -0,0 +1,18 @@ +@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#> . + +<http://lv2plug.in/plugins/eg-scope#Mono> + a lv2:Plugin ; + lv2:binary <examploscope@LIB_EXT@> ; + rdfs:seeAlso <examploscope.ttl> . + +<http://lv2plug.in/plugins/eg-scope#Stereo> + a lv2:Plugin ; + lv2:binary <examploscope@LIB_EXT@> ; + rdfs:seeAlso <examploscope.ttl> . + +<http://lv2plug.in/plugins/eg-scope#ui> + a ui:GtkUI ; + ui:binary <examploscope_ui@LIB_EXT@> ; + rdfs:seeAlso <examploscope.ttl> . diff --git a/plugins/eg05-scope.lv2/uris.h b/plugins/eg05-scope.lv2/uris.h new file mode 100644 index 0000000..bd57551 --- /dev/null +++ b/plugins/eg05-scope.lv2/uris.h @@ -0,0 +1,73 @@ +/* + 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/lv2plug.in/ns/ext/atom/atom.h" +#include "lv2/lv2plug.in/ns/ext/atom/forge.h" +#include "lv2/lv2plug.in/ns/ext/parameters/parameters.h" +#include "lv2/lv2plug.in/ns/ext/urid/urid.h" + +#define SCO_URI "http://lv2plug.in/plugins/eg-scope" + +typedef struct { + // URIs defined in LV2 specifications + LV2_URID atom_Blank; + 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_Blank = map->map(map->handle, LV2_ATOM__Blank); + 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/eg05-scope.lv2/wscript b/plugins/eg05-scope.lv2/wscript new file mode 100644 index 0000000..807d15d --- /dev/null +++ b/plugins/eg05-scope.lv2/wscript @@ -0,0 +1,73 @@ +#!/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') + autowaf.set_options(opt) + +def configure(conf): + conf.load('compiler_c') + autowaf.configure(conf) + autowaf.set_c99_mode(conf) + autowaf.display_header('Scope Configuration') + + 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) + + autowaf.display_msg(conf, 'LV2 bundle directory', conf.env.LV2DIR) + print('') + +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.is_defined('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 |