aboutsummaryrefslogtreecommitdiffstats
path: root/plugins/eg-sampler.lv2
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/eg-sampler.lv2')
-rw-r--r--plugins/eg-sampler.lv2/README.txt13
-rw-r--r--plugins/eg-sampler.lv2/click.wavbin0 -> 644 bytes
-rw-r--r--plugins/eg-sampler.lv2/manifest.ttl.in19
-rw-r--r--plugins/eg-sampler.lv2/sampler.c483
-rw-r--r--plugins/eg-sampler.lv2/sampler.ttl67
-rw-r--r--plugins/eg-sampler.lv2/sampler_ui.c241
-rw-r--r--plugins/eg-sampler.lv2/uris.h135
l---------plugins/eg-sampler.lv2/waf1
-rw-r--r--plugins/eg-sampler.lv2/wscript80
9 files changed, 1039 insertions, 0 deletions
diff --git a/plugins/eg-sampler.lv2/README.txt b/plugins/eg-sampler.lv2/README.txt
new file mode 100644
index 0000000..4eed9e6
--- /dev/null
+++ b/plugins/eg-sampler.lv2/README.txt
@@ -0,0 +1,13 @@
+== Sampler ==
+
+This plugin loads a single sample from a .wav file and plays it back when a MIDI
+note on is received. Any sample on the system can be loaded via another event.
+A Gtk UI is included which does this, but the host can as well.
+
+This plugin illustrates:
+
+- UI <==> Plugin communication via events
+- Use of the worker extension for non-realtime tasks (sample loading)
+- Use of the log extension to print log messages via the host
+- Saving plugin state via the state extension
+- Dynamic plugin control via the same properties saved to state
diff --git a/plugins/eg-sampler.lv2/click.wav b/plugins/eg-sampler.lv2/click.wav
new file mode 100644
index 0000000..520a18c
--- /dev/null
+++ b/plugins/eg-sampler.lv2/click.wav
Binary files differ
diff --git a/plugins/eg-sampler.lv2/manifest.ttl.in b/plugins/eg-sampler.lv2/manifest.ttl.in
new file mode 100644
index 0000000..8a01428
--- /dev/null
+++ b/plugins/eg-sampler.lv2/manifest.ttl.in
@@ -0,0 +1,19 @@
+# Unlike the previous examples, this manifest lists more than one resource: the
+# plugin as usual, and the UI. The descriptions are similar, but have
+# different types, so the host can decide from this file alone whether or not
+# it is interested, and avoid following the `rdfs:seeAlso` link if not (though
+# in this case both are described in the same file).
+
+@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-sampler>
+ a lv2:Plugin ;
+ lv2:binary <sampler@LIB_EXT@> ;
+ rdfs:seeAlso <sampler.ttl> .
+
+<http://lv2plug.in/plugins/eg-sampler#ui>
+ a ui:GtkUI ;
+ ui:binary <sampler_ui@LIB_EXT@> ;
+ rdfs:seeAlso <sampler.ttl> .
diff --git a/plugins/eg-sampler.lv2/sampler.c b/plugins/eg-sampler.lv2/sampler.c
new file mode 100644
index 0000000..54da799
--- /dev/null
+++ b/plugins/eg-sampler.lv2/sampler.c
@@ -0,0 +1,483 @@
+/*
+ LV2 Sampler Example Plugin
+ Copyright 2011-2012 David Robillard <d@drobilla.net>
+ Copyright 2011 Gabriel M. Beddingfield <gabriel@teuton.org>
+ Copyright 2011 James Morris <jwm.art.net@gmail.com>
+
+ 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 <math.h>
+#include <stdlib.h>
+#include <string.h>
+#ifndef __cplusplus
+# include <stdbool.h>
+#endif
+
+#include <sndfile.h>
+
+#include "lv2/lv2plug.in/ns/ext/atom/forge.h"
+#include "lv2/lv2plug.in/ns/ext/atom/util.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/midi/midi.h"
+#include "lv2/lv2plug.in/ns/ext/patch/patch.h"
+#include "lv2/lv2plug.in/ns/ext/state/state.h"
+#include "lv2/lv2plug.in/ns/ext/urid/urid.h"
+#include "lv2/lv2plug.in/ns/ext/worker/worker.h"
+#include "lv2/lv2plug.in/ns/lv2core/lv2.h"
+
+#include "./uris.h"
+
+enum {
+ SAMPLER_CONTROL = 0,
+ SAMPLER_NOTIFY = 1,
+ SAMPLER_OUT = 2
+};
+
+static const char* default_sample_file = "click.wav";
+
+typedef struct {
+ SF_INFO info; // Info about sample from sndfile
+ float* data; // Sample data in float
+ char* path; // Path of file
+ uint32_t path_len; // Length of path
+} Sample;
+
+typedef struct {
+ // Features
+ LV2_URID_Map* map;
+ LV2_Worker_Schedule* schedule;
+ LV2_Log_Log* log;
+
+ // Forge for creating atoms
+ LV2_Atom_Forge forge;
+
+ // Logger convenience API
+ LV2_Log_Logger logger;
+
+ // Sample
+ Sample* sample;
+
+ // Ports
+ const LV2_Atom_Sequence* control_port;
+ LV2_Atom_Sequence* notify_port;
+ float* output_port;
+
+ // Forge frame for notify port (for writing worker replies)
+ LV2_Atom_Forge_Frame notify_frame;
+
+ // URIs
+ SamplerURIs uris;
+
+ // Current position in run()
+ uint32_t frame_offset;
+
+ // Playback state
+ sf_count_t frame;
+ bool play;
+} Sampler;
+
+/**
+ An atom-like message used internally to apply/free samples.
+
+ This is only used internally to communicate with the worker, it is never
+ sent to the outside world via a port since it is not POD. It is convenient
+ to use an Atom header so actual atoms can be easily sent through the same
+ ringbuffer.
+*/
+typedef struct {
+ LV2_Atom atom;
+ Sample* sample;
+} SampleMessage;
+
+/**
+ Load a new sample and return it.
+
+ Since this is of course not a real-time safe action, this is called in the
+ worker thread only. The sample is loaded and returned only, plugin state is
+ not modified.
+*/
+static Sample*
+load_sample(Sampler* self, const char* path)
+{
+ const size_t path_len = strlen(path);
+
+ lv2_log_trace(&self->logger, "Loading sample %s\n", path);
+
+ Sample* const sample = (Sample*)malloc(sizeof(Sample));
+ SF_INFO* const info = &sample->info;
+ SNDFILE* const sndfile = sf_open(path, SFM_READ, info);
+
+ if (!sndfile || !info->frames || (info->channels != 1)) {
+ lv2_log_error(&self->logger, "Failed to open sample '%s'\n", path);
+ free(sample);
+ return NULL;
+ }
+
+ // Read data
+ float* const data = malloc(sizeof(float) * info->frames);
+ if (!data) {
+ lv2_log_error(&self->logger, "Failed to allocate memory for sample\n");
+ return NULL;
+ }
+ sf_seek(sndfile, 0ul, SEEK_SET);
+ sf_read_float(sndfile, data, info->frames);
+ sf_close(sndfile);
+
+ // Fill sample struct and return it
+ sample->data = data;
+ sample->path = (char*)malloc(path_len + 1);
+ sample->path_len = (uint32_t)path_len;
+ memcpy(sample->path, path, path_len + 1);
+
+ return sample;
+}
+
+static void
+free_sample(Sampler* self, Sample* sample)
+{
+ if (sample) {
+ lv2_log_trace(&self->logger, "Freeing %s\n", sample->path);
+ free(sample->path);
+ free(sample->data);
+ free(sample);
+ }
+}
+
+/**
+ Do work in a non-realtime thread.
+
+ This is called for every piece of work scheduled in the audio thread using
+ self->schedule->schedule_work(). A reply can be sent back to the audio
+ thread using the provided respond function.
+*/
+static LV2_Worker_Status
+work(LV2_Handle instance,
+ LV2_Worker_Respond_Function respond,
+ LV2_Worker_Respond_Handle handle,
+ uint32_t size,
+ const void* data)
+{
+ Sampler* self = (Sampler*)instance;
+ const LV2_Atom* atom = (const LV2_Atom*)data;
+ if (atom->type == self->uris.eg_freeSample) {
+ // Free old sample
+ const SampleMessage* msg = (const SampleMessage*)data;
+ free_sample(self, msg->sample);
+ } else {
+ // Handle set message (load sample).
+ const LV2_Atom_Object* obj = (const LV2_Atom_Object*)data;
+
+ // Get file path from message
+ const LV2_Atom* file_path = read_set_file(&self->uris, obj);
+ if (!file_path) {
+ return LV2_WORKER_ERR_UNKNOWN;
+ }
+
+ // Load sample.
+ Sample* sample = load_sample(self, LV2_ATOM_BODY_CONST(file_path));
+ if (sample) {
+ // Loaded sample, send it to run() to be applied.
+ respond(handle, sizeof(sample), &sample);
+ }
+ }
+
+ return LV2_WORKER_SUCCESS;
+}
+
+/**
+ Handle a response from work() in the audio thread.
+
+ When running normally, this will be called by the host after run(). When
+ freewheeling, this will be called immediately at the point the work was
+ scheduled.
+*/
+static LV2_Worker_Status
+work_response(LV2_Handle instance,
+ uint32_t size,
+ const void* data)
+{
+ Sampler* self = (Sampler*)instance;
+
+ SampleMessage msg = { { sizeof(Sample*), self->uris.eg_freeSample },
+ self->sample };
+
+ // Send a message to the worker to free the current sample
+ self->schedule->schedule_work(self->schedule->handle, sizeof(msg), &msg);
+
+ // Install the new sample
+ self->sample = *(Sample*const*)data;
+
+ // Send a notification that we're using a new sample.
+ lv2_atom_forge_frame_time(&self->forge, self->frame_offset);
+ write_set_file(&self->forge, &self->uris,
+ self->sample->path,
+ self->sample->path_len);
+
+ return LV2_WORKER_SUCCESS;
+}
+
+static void
+connect_port(LV2_Handle instance,
+ uint32_t port,
+ void* data)
+{
+ Sampler* self = (Sampler*)instance;
+ switch (port) {
+ case SAMPLER_CONTROL:
+ self->control_port = (const LV2_Atom_Sequence*)data;
+ break;
+ case SAMPLER_NOTIFY:
+ self->notify_port = (LV2_Atom_Sequence*)data;
+ break;
+ case SAMPLER_OUT:
+ self->output_port = (float*)data;
+ break;
+ default:
+ break;
+ }
+}
+
+static LV2_Handle
+instantiate(const LV2_Descriptor* descriptor,
+ double rate,
+ const char* path,
+ const LV2_Feature* const* features)
+{
+ // Allocate and initialise instance structure.
+ Sampler* self = (Sampler*)malloc(sizeof(Sampler));
+ if (!self) {
+ return NULL;
+ }
+ memset(self, 0, sizeof(Sampler));
+
+ // 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_WORKER__schedule)) {
+ self->schedule = (LV2_Worker_Schedule*)features[i]->data;
+ } else if (!strcmp(features[i]->URI, LV2_LOG__log)) {
+ self->log = (LV2_Log_Log*)features[i]->data;
+ }
+ }
+ if (!self->map) {
+ lv2_log_error(&self->logger, "Missing feature urid:map\n");
+ goto fail;
+ } else if (!self->schedule) {
+ lv2_log_error(&self->logger, "Missing feature work:schedule\n");
+ goto fail;
+ }
+
+ // Map URIs and initialise forge/logger
+ map_sampler_uris(self->map, &self->uris);
+ lv2_atom_forge_init(&self->forge, self->map);
+ lv2_log_logger_init(&self->logger, self->map, self->log);
+
+ // Load the default sample file
+ const size_t path_len = strlen(path);
+ const size_t file_len = strlen(default_sample_file);
+ const size_t len = path_len + file_len;
+ char* sample_path = (char*)malloc(len + 1);
+ snprintf(sample_path, len + 1, "%s%s", path, default_sample_file);
+ self->sample = load_sample(self, sample_path);
+ free(sample_path);
+
+ return (LV2_Handle)self;
+
+fail:
+ free(self);
+ return 0;
+}
+
+static void
+cleanup(LV2_Handle instance)
+{
+ Sampler* self = (Sampler*)instance;
+ free_sample(self, self->sample);
+ free(self);
+}
+
+static void
+run(LV2_Handle instance,
+ uint32_t sample_count)
+{
+ Sampler* self = (Sampler*)instance;
+ SamplerURIs* uris = &self->uris;
+ sf_count_t start_frame = 0;
+ sf_count_t pos = 0;
+ float* output = self->output_port;
+
+ // Set up forge to write directly to notify output port.
+ const uint32_t notify_capacity = self->notify_port->atom.size;
+ lv2_atom_forge_set_buffer(&self->forge,
+ (uint8_t*)self->notify_port,
+ notify_capacity);
+
+ // Start a sequence in the notify output port.
+ lv2_atom_forge_sequence_head(&self->forge, &self->notify_frame, 0);
+
+ // Read incoming events
+ LV2_ATOM_SEQUENCE_FOREACH(self->control_port, ev) {
+ self->frame_offset = ev->time.frames;
+ if (ev->body.type == uris->midi_Event) {
+ const uint8_t* const msg = (const uint8_t*)(ev + 1);
+ switch (lv2_midi_message_type(msg)) {
+ case LV2_MIDI_MSG_NOTE_ON:
+ start_frame = ev->time.frames;
+ self->frame = 0;
+ self->play = true;
+ break;
+ default:
+ break;
+ }
+ } else 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 == uris->patch_Set) {
+ // Received a set message, send it to the worker.
+ lv2_log_trace(&self->logger, "Queueing set message\n");
+ self->schedule->schedule_work(self->schedule->handle,
+ lv2_atom_total_size(&ev->body),
+ &ev->body);
+ } else {
+ lv2_log_trace(&self->logger,
+ "Unknown object type %d\n", obj->body.otype);
+ }
+ } else {
+ lv2_log_trace(&self->logger,
+ "Unknown event type %d\n", ev->body.type);
+ }
+ }
+
+ // Render the sample (possibly already in progress)
+ if (self->play) {
+ uint32_t f = self->frame;
+ const uint32_t lf = self->sample->info.frames;
+
+ for (pos = 0; pos < start_frame; ++pos) {
+ output[pos] = 0;
+ }
+
+ for (; pos < sample_count && f < lf; ++pos, ++f) {
+ output[pos] = self->sample->data[f];
+ }
+
+ self->frame = f;
+
+ if (f == lf) {
+ self->play = false;
+ }
+ }
+
+ // Add zeros to end if sample not long enough (or not playing)
+ for (; pos < sample_count; ++pos) {
+ output[pos] = 0.0f;
+ }
+}
+
+static LV2_State_Status
+save(LV2_Handle instance,
+ LV2_State_Store_Function store,
+ LV2_State_Handle handle,
+ uint32_t flags,
+ const LV2_Feature* const* features)
+{
+ Sampler* self = (Sampler*)instance;
+ if (!self->sample) {
+ return LV2_STATE_SUCCESS;
+ }
+
+ LV2_State_Map_Path* map_path = NULL;
+ for (int i = 0; features[i]; ++i) {
+ if (!strcmp(features[i]->URI, LV2_STATE__mapPath)) {
+ map_path = (LV2_State_Map_Path*)features[i]->data;
+ }
+ }
+
+ char* apath = map_path->abstract_path(map_path->handle, self->sample->path);
+
+ store(handle,
+ self->uris.eg_sample,
+ apath,
+ strlen(self->sample->path) + 1,
+ self->uris.atom_Path,
+ LV2_STATE_IS_POD | LV2_STATE_IS_PORTABLE);
+
+ free(apath);
+
+ return LV2_STATE_SUCCESS;
+}
+
+static LV2_State_Status
+restore(LV2_Handle instance,
+ LV2_State_Retrieve_Function retrieve,
+ LV2_State_Handle handle,
+ uint32_t flags,
+ const LV2_Feature* const* features)
+{
+ Sampler* self = (Sampler*)instance;
+
+ size_t size;
+ uint32_t type;
+ uint32_t valflags;
+
+ const void* value = retrieve(
+ handle,
+ self->uris.eg_sample,
+ &size, &type, &valflags);
+
+ if (value) {
+ const char* path = (const char*)value;
+ lv2_log_trace(&self->logger, "Restoring file %s\n", path);
+ free_sample(self, self->sample);
+ self->sample = load_sample(self, path);
+ }
+
+ return LV2_STATE_SUCCESS;
+}
+
+static const void*
+extension_data(const char* uri)
+{
+ static const LV2_State_Interface state = { save, restore };
+ static const LV2_Worker_Interface worker = { work, work_response, NULL };
+ if (!strcmp(uri, LV2_STATE__interface)) {
+ return &state;
+ } else if (!strcmp(uri, LV2_WORKER__interface)) {
+ return &worker;
+ }
+ return NULL;
+}
+
+static const LV2_Descriptor descriptor = {
+ EG_SAMPLER_URI,
+ instantiate,
+ connect_port,
+ NULL, // activate,
+ run,
+ NULL, // deactivate,
+ cleanup,
+ extension_data
+};
+
+LV2_SYMBOL_EXPORT
+const LV2_Descriptor* lv2_descriptor(uint32_t index)
+{
+ switch (index) {
+ case 0:
+ return &descriptor;
+ default:
+ return NULL;
+ }
+}
diff --git a/plugins/eg-sampler.lv2/sampler.ttl b/plugins/eg-sampler.lv2/sampler.ttl
new file mode 100644
index 0000000..2a89dd2
--- /dev/null
+++ b/plugins/eg-sampler.lv2/sampler.ttl
@@ -0,0 +1,67 @@
+@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
+@prefix doap: <http://usefulinc.com/ns/doap#> .
+@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
+@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix state: <http://lv2plug.in/ns/ext/state#> .
+@prefix ui: <http://lv2plug.in/ns/extensions/ui#> .
+@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
+@prefix work: <http://lv2plug.in/ns/ext/worker#> .
+
+<http://lv2plug.in/plugins/eg-sampler#sample>
+ a lv2:Parameter ;
+ rdfs:label "sample" ;
+ rdfs:range atom:Path .
+
+<http://lv2plug.in/plugins/eg-sampler>
+ a lv2:Plugin ;
+ doap:name "Example Sampler" ;
+ doap:license <http://opensource.org/licenses/isc> ;
+ lv2:project <http://lv2plug.in/ns/lv2> ;
+ lv2:requiredFeature urid:map ,
+ work:schedule ;
+ lv2:optionalFeature lv2:hardRTCapable ,
+ state:loadDefaultState ;
+ lv2:extensionData state:interface ,
+ work:interface ;
+ ui:ui <http://lv2plug.in/plugins/eg-sampler#ui> ;
+ patch:writable <http://lv2plug.in/plugins/eg-sampler#sample> ;
+ lv2:port [
+ a lv2:InputPort ,
+ atom:AtomPort ;
+ atom:bufferType atom:Sequence ;
+ atom:supports <http://lv2plug.in/ns/ext/midi#MidiEvent> ,
+ patch:Message ;
+ lv2:designation lv2:control ;
+ lv2:index 0 ;
+ lv2:symbol "control" ;
+ lv2:name "Control"
+ ] , [
+ a lv2:OutputPort ,
+ atom:AtomPort ;
+ atom:bufferType atom:Sequence ;
+ atom:supports patch:Message ;
+ lv2:designation lv2:control ;
+ lv2:index 1 ;
+ lv2:symbol "notify" ;
+ lv2:name "Notify"
+ ] , [
+ a lv2:AudioPort ,
+ lv2:OutputPort ;
+ lv2:index 2 ;
+ lv2:symbol "out" ;
+ lv2:name "Out"
+ ] ;
+ state:state [
+ <http://lv2plug.in/plugins/eg-sampler#sample> <click.wav>
+ ] .
+
+<http://lv2plug.in/plugins/eg-sampler#ui>
+ a ui:GtkUI ;
+ lv2:requiredFeature urid:map ;
+ lv2:extensionData ui:showInterface ;
+ ui:portNotification [
+ ui:plugin <http://lv2plug.in/plugins/eg-sampler> ;
+ lv2:symbol "notify" ;
+ ui:notifyType atom:Blank
+ ] .
diff --git a/plugins/eg-sampler.lv2/sampler_ui.c b/plugins/eg-sampler.lv2/sampler_ui.c
new file mode 100644
index 0000000..d691c98
--- /dev/null
+++ b/plugins/eg-sampler.lv2/sampler_ui.c
@@ -0,0 +1,241 @@
+/*
+ LV2 Sampler Example Plugin UI
+ Copyright 2011-2012 David Robillard <d@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.
+*/
+
+#include <stdlib.h>
+
+#include <gtk/gtk.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/atom/util.h"
+#include "lv2/lv2plug.in/ns/ext/patch/patch.h"
+#include "lv2/lv2plug.in/ns/ext/urid/urid.h"
+#include "lv2/lv2plug.in/ns/extensions/ui/ui.h"
+
+#include "./uris.h"
+
+#define SAMPLER_UI_URI "http://lv2plug.in/plugins/eg-sampler#ui"
+
+typedef struct {
+ LV2_Atom_Forge forge;
+
+ LV2_URID_Map* map;
+ SamplerURIs uris;
+
+ LV2UI_Write_Function write;
+ LV2UI_Controller controller;
+
+ GtkWidget* box;
+ GtkWidget* button;
+ GtkWidget* label;
+ GtkWidget* window; /* For optional show interface. */
+} SamplerUI;
+
+static void
+on_load_clicked(GtkWidget* widget,
+ void* handle)
+{
+ SamplerUI* ui = (SamplerUI*)handle;
+
+ /* Create a dialog to select a sample file. */
+ GtkWidget* dialog = gtk_file_chooser_dialog_new(
+ "Load Sample",
+ NULL,
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+ GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ /* Run the dialog, and return if it is cancelled. */
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) != GTK_RESPONSE_ACCEPT) {
+ gtk_widget_destroy(dialog);
+ return;
+ }
+
+ /* Get the file path from the dialog. */
+ char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+
+ /* Got what we need, destroy the dialog. */
+ gtk_widget_destroy(dialog);
+
+#define OBJ_BUF_SIZE 1024
+ uint8_t obj_buf[OBJ_BUF_SIZE];
+ lv2_atom_forge_set_buffer(&ui->forge, obj_buf, OBJ_BUF_SIZE);
+
+ LV2_Atom* msg = write_set_file(&ui->forge, &ui->uris,
+ filename, strlen(filename));
+
+ ui->write(ui->controller, 0, lv2_atom_total_size(msg),
+ ui->uris.atom_eventTransfer,
+ msg);
+
+ g_free(filename);
+}
+
+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)
+{
+ SamplerUI* ui = (SamplerUI*)malloc(sizeof(SamplerUI));
+ ui->map = NULL;
+ ui->write = write_function;
+ ui->controller = controller;
+ ui->box = NULL;
+ ui->button = NULL;
+ ui->label = NULL;
+ ui->window = NULL;
+
+ *widget = 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, "sampler_ui: Host does not support urid:Map\n");
+ free(ui);
+ return NULL;
+ }
+
+ map_sampler_uris(ui->map, &ui->uris);
+
+ lv2_atom_forge_init(&ui->forge, ui->map);
+
+ ui->box = gtk_vbox_new(FALSE, 4);
+ ui->label = gtk_label_new("?");
+ ui->button = gtk_button_new_with_label("Load Sample");
+ gtk_box_pack_start(GTK_BOX(ui->box), ui->label, TRUE, TRUE, 4);
+ gtk_box_pack_start(GTK_BOX(ui->box), ui->button, FALSE, FALSE, 4);
+ g_signal_connect(ui->button, "clicked",
+ G_CALLBACK(on_load_clicked),
+ ui);
+
+ *widget = ui->box;
+
+ return ui;
+}
+
+static void
+cleanup(LV2UI_Handle handle)
+{
+ SamplerUI* ui = (SamplerUI*)handle;
+ gtk_widget_destroy(ui->button);
+ free(ui);
+}
+
+static void
+port_event(LV2UI_Handle handle,
+ uint32_t port_index,
+ uint32_t buffer_size,
+ uint32_t format,
+ const void* buffer)
+{
+ SamplerUI* ui = (SamplerUI*)handle;
+ if (format == ui->uris.atom_eventTransfer) {
+ const LV2_Atom* atom = (const LV2_Atom*)buffer;
+ if (atom->type == ui->uris.atom_Blank) {
+ const LV2_Atom_Object* obj = (const LV2_Atom_Object*)atom;
+ const LV2_Atom* file_uri = read_set_file(&ui->uris, obj);
+ if (!file_uri) {
+ fprintf(stderr, "Unknown message sent to UI.\n");
+ return;
+ }
+
+ const char* uri = (const char*)LV2_ATOM_BODY_CONST(file_uri);
+ gtk_label_set_text(GTK_LABEL(ui->label), uri);
+ } else {
+ fprintf(stderr, "Unknown message type.\n");
+ }
+ } else {
+ fprintf(stderr, "Unknown format.\n");
+ }
+}
+
+/* Optional non-embedded UI show interface. */
+static int
+ui_show(LV2UI_Handle handle)
+{
+ SamplerUI* ui = (SamplerUI*)handle;
+
+ int argc = 0;
+ gtk_init(&argc, NULL);
+
+ ui->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_container_add(GTK_CONTAINER(ui->window), ui->box);
+ gtk_widget_show_all(ui->window);
+ gtk_window_present(GTK_WINDOW(ui->window));
+
+ return 0;
+}
+
+/* Optional non-embedded UI hide interface. */
+static int
+ui_hide(LV2UI_Handle handle)
+{
+ return 0;
+}
+
+/* Idle interface for optional non-embedded UI. */
+static int
+ui_idle(LV2UI_Handle handle)
+{
+ SamplerUI* ui = (SamplerUI*)handle;
+ if (ui->window) {
+ gtk_main_iteration();
+ }
+ return 0;
+}
+
+static const void*
+extension_data(const char* uri)
+{
+ static const LV2UI_Show_Interface show = { ui_show, ui_hide };
+ static const LV2UI_Idle_Interface idle = { ui_idle };
+ if (!strcmp(uri, LV2_UI__showInterface)) {
+ return &show;
+ } else if (!strcmp(uri, LV2_UI__idleInterface)) {
+ return &idle;
+ }
+ return NULL;
+}
+
+static const LV2UI_Descriptor descriptor = {
+ SAMPLER_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 NULL;
+ }
+}
diff --git a/plugins/eg-sampler.lv2/uris.h b/plugins/eg-sampler.lv2/uris.h
new file mode 100644
index 0000000..8e9faee
--- /dev/null
+++ b/plugins/eg-sampler.lv2/uris.h
@@ -0,0 +1,135 @@
+/*
+ LV2 Sampler Example Plugin
+ Copyright 2011-2012 David Robillard <d@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.
+*/
+
+#ifndef SAMPLER_URIS_H
+#define SAMPLER_URIS_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/state/state.h"
+
+#define EG_SAMPLER_URI "http://lv2plug.in/plugins/eg-sampler"
+#define EG_SAMPLER__sample EG_SAMPLER_URI "#sample"
+#define EG_SAMPLER__applySample EG_SAMPLER_URI "#applySample"
+#define EG_SAMPLER__freeSample EG_SAMPLER_URI "#freeSample"
+
+typedef struct {
+ LV2_URID atom_Blank;
+ LV2_URID atom_Path;
+ LV2_URID atom_Resource;
+ LV2_URID atom_Sequence;
+ LV2_URID atom_URID;
+ LV2_URID atom_eventTransfer;
+ LV2_URID eg_applySample;
+ LV2_URID eg_sample;
+ LV2_URID eg_freeSample;
+ LV2_URID midi_Event;
+ LV2_URID patch_Set;
+ LV2_URID patch_property;
+ LV2_URID patch_value;
+} SamplerURIs;
+
+static inline void
+map_sampler_uris(LV2_URID_Map* map, SamplerURIs* uris)
+{
+ uris->atom_Blank = map->map(map->handle, LV2_ATOM__Blank);
+ uris->atom_Path = map->map(map->handle, LV2_ATOM__Path);
+ uris->atom_Resource = map->map(map->handle, LV2_ATOM__Resource);
+ uris->atom_Sequence = map->map(map->handle, LV2_ATOM__Sequence);
+ uris->atom_URID = map->map(map->handle, LV2_ATOM__URID);
+ uris->atom_eventTransfer = map->map(map->handle, LV2_ATOM__eventTransfer);
+ uris->eg_applySample = map->map(map->handle, EG_SAMPLER__applySample);
+ uris->eg_freeSample = map->map(map->handle, EG_SAMPLER__freeSample);
+ uris->eg_sample = map->map(map->handle, EG_SAMPLER__sample);
+ uris->midi_Event = map->map(map->handle, LV2_MIDI__MidiEvent);
+ uris->patch_Set = map->map(map->handle, LV2_PATCH__Set);
+ uris->patch_property = map->map(map->handle, LV2_PATCH__property);
+ uris->patch_value = map->map(map->handle, LV2_PATCH__value);
+}
+
+/**
+ * Write a message like the following to @p forge:
+ * []
+ * a patch:Set ;
+ * patch:property eg:sample ;
+ * patch:value </home/me/foo.wav> .
+ */
+static inline LV2_Atom*
+write_set_file(LV2_Atom_Forge* forge,
+ const SamplerURIs* uris,
+ const char* filename,
+ const uint32_t filename_len)
+{
+ LV2_Atom_Forge_Frame frame;
+ LV2_Atom* set = (LV2_Atom*)lv2_atom_forge_object(
+ forge, &frame, 0, uris->patch_Set);
+
+ lv2_atom_forge_key(forge, uris->patch_property);
+ lv2_atom_forge_urid(forge, uris->eg_sample);
+ lv2_atom_forge_key(forge, uris->patch_value);
+ lv2_atom_forge_path(forge, filename, filename_len);
+
+ lv2_atom_forge_pop(forge, &frame);
+
+ return set;
+}
+
+/**
+ * Get the file path from a message like:
+ * []
+ * a patch:Set ;
+ * patch:property eg:sample ;
+ * patch:value </home/me/foo.wav> .
+ */
+static inline const LV2_Atom*
+read_set_file(const SamplerURIs* uris,
+ const LV2_Atom_Object* obj)
+{
+ if (obj->body.otype != uris->patch_Set) {
+ fprintf(stderr, "Ignoring unknown message type %d\n", obj->body.otype);
+ return NULL;
+ }
+
+ /* Get property URI. */
+ const LV2_Atom* property = NULL;
+ lv2_atom_object_get(obj, uris->patch_property, &property, 0);
+ if (!property) {
+ fprintf(stderr, "Malformed set message has no body.\n");
+ return NULL;
+ } else if (property->type != uris->atom_URID) {
+ fprintf(stderr, "Malformed set message has non-URID property.\n");
+ return NULL;
+ } else if (((const LV2_Atom_URID*)property)->body != uris->eg_sample) {
+ fprintf(stderr, "Set message for unknown property.\n");
+ return NULL;
+ }
+
+ /* Get value. */
+ const LV2_Atom* file_path = NULL;
+ lv2_atom_object_get(obj, uris->patch_value, &file_path, 0);
+ if (!file_path) {
+ fprintf(stderr, "Malformed set message has no value.\n");
+ return NULL;
+ } else if (file_path->type != uris->atom_Path) {
+ fprintf(stderr, "Set message value is not a Path.\n");
+ return NULL;
+ }
+
+ return file_path;
+}
+
+#endif /* SAMPLER_URIS_H */
diff --git a/plugins/eg-sampler.lv2/waf b/plugins/eg-sampler.lv2/waf
new file mode 120000
index 0000000..59a1ac9
--- /dev/null
+++ b/plugins/eg-sampler.lv2/waf
@@ -0,0 +1 @@
+../../waf \ No newline at end of file
diff --git a/plugins/eg-sampler.lv2/wscript b/plugins/eg-sampler.lv2/wscript
new file mode 100644
index 0000000..732c904
--- /dev/null
+++ b/plugins/eg-sampler.lv2/wscript
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+from waflib.extras import autowaf as autowaf
+import re
+
+# Variables for 'waf dist'
+APPNAME = 'eg-sampler.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('Sampler Configuration')
+
+ if not autowaf.is_child():
+ autowaf.check_pkg(conf, 'lv2', atleast_version='1.2.1', uselib_store='LV2')
+
+ autowaf.check_pkg(conf, 'sndfile', uselib_store='SNDFILE',
+ atleast_version='1.0.0', 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-sampler.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)
+ bld(features = 'subst',
+ source = 'manifest.ttl.in',
+ target = '%s/%s' % (bundle, 'manifest.ttl'),
+ install_path = '${LV2DIR}/%s' % bundle,
+ LIB_EXT = module_ext)
+
+ # Copy other data files to build bundle (build/eg-sampler.lv2)
+ for i in ['sampler.ttl', 'click.wav']:
+ bld(features = 'subst',
+ is_copy = True,
+ source = i,
+ target = '%s/%s' % (bundle, i),
+ install_path = '${LV2DIR}/%s' % bundle)
+
+ # 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 = 'sampler.c',
+ name = 'sampler',
+ target = '%s/sampler' % bundle,
+ install_path = '${LV2DIR}/%s' % bundle,
+ use = 'SNDFILE LV2',
+ includes = includes)
+ obj.env.cshlib_PATTERN = module_pat
+
+ # Build UI library
+ if bld.is_defined('HAVE_GTK2'):
+ obj = bld(features = 'c cshlib',
+ source = 'sampler_ui.c',
+ name = 'sampler_ui',
+ target = '%s/sampler_ui' % bundle,
+ install_path = '${LV2DIR}/%s' % bundle,
+ use = 'GTK2 LV2',
+ includes = includes)
+ obj.env.cshlib_PATTERN = module_pat