diff options
Diffstat (limited to 'plugins/eg-sampler.lv2')
-rw-r--r-- | plugins/eg-sampler.lv2/README.txt | 13 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/click.wav | bin | 0 -> 644 bytes | |||
-rw-r--r-- | plugins/eg-sampler.lv2/manifest.ttl.in | 19 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/sampler.c | 483 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/sampler.ttl | 67 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/sampler_ui.c | 241 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/uris.h | 135 | ||||
l--------- | plugins/eg-sampler.lv2/waf | 1 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/wscript | 80 |
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 Binary files differnew file mode 100644 index 0000000..520a18c --- /dev/null +++ b/plugins/eg-sampler.lv2/click.wav 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 |