From 8a765b347a994cee18412823de590de4af0dfe3f Mon Sep 17 00:00:00 2001 From: Holger Wirtz Date: Fri, 23 Sep 2016 15:10:33 +0200 Subject: [PATCH] Added some examples... --- .gitignore | 1 + {beep => lv2-examples/beep}/Makefile | 0 {beep => lv2-examples/beep}/beep.cpp | 2 + {beep => lv2-examples/beep}/beep.peg | 0 {beep => lv2-examples/beep}/beep.ttl | 0 .../beep}/lv2pftci-beep.lv2/beep.ttl | 0 .../beep}/lv2pftci-beep.lv2/manifest.ttl | 0 {beep => lv2-examples/beep}/manifest.ttl | 0 lv2-examples/eg-sampler.lv2/README.txt | 13 + lv2-examples/eg-sampler.lv2/atom_sink.h | 40 ++ lv2-examples/eg-sampler.lv2/click.wav | Bin 0 -> 644 bytes lv2-examples/eg-sampler.lv2/manifest.ttl.in | 19 + lv2-examples/eg-sampler.lv2/sampler.c | 552 ++++++++++++++++++ lv2-examples/eg-sampler.lv2/sampler.ttl | 70 +++ lv2-examples/eg-sampler.lv2/sampler_ui.c | 254 ++++++++ lv2-examples/eg-sampler.lv2/uris.h | 139 +++++ lv2-examples/eg-sampler.lv2/waf | 1 + lv2-examples/eg-sampler.lv2/wscript | 83 +++ src/.Makefile.swp | Bin 12288 -> 0 bytes 19 files changed, 1174 insertions(+) rename {beep => lv2-examples/beep}/Makefile (100%) rename {beep => lv2-examples/beep}/beep.cpp (95%) rename {beep => lv2-examples/beep}/beep.peg (100%) rename {beep => lv2-examples/beep}/beep.ttl (100%) rename {beep => lv2-examples/beep}/lv2pftci-beep.lv2/beep.ttl (100%) rename {beep => lv2-examples/beep}/lv2pftci-beep.lv2/manifest.ttl (100%) rename {beep => lv2-examples/beep}/manifest.ttl (100%) create mode 100644 lv2-examples/eg-sampler.lv2/README.txt create mode 100644 lv2-examples/eg-sampler.lv2/atom_sink.h create mode 100644 lv2-examples/eg-sampler.lv2/click.wav create mode 100644 lv2-examples/eg-sampler.lv2/manifest.ttl.in create mode 100644 lv2-examples/eg-sampler.lv2/sampler.c create mode 100644 lv2-examples/eg-sampler.lv2/sampler.ttl create mode 100644 lv2-examples/eg-sampler.lv2/sampler_ui.c create mode 100644 lv2-examples/eg-sampler.lv2/uris.h create mode 120000 lv2-examples/eg-sampler.lv2/waf create mode 100644 lv2-examples/eg-sampler.lv2/wscript delete mode 100644 src/.Makefile.swp diff --git a/.gitignore b/.gitignore index 283611c..4613747 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.o *.so *.gch +*.swp diff --git a/beep/Makefile b/lv2-examples/beep/Makefile similarity index 100% rename from beep/Makefile rename to lv2-examples/beep/Makefile diff --git a/beep/beep.cpp b/lv2-examples/beep/beep.cpp similarity index 95% rename from beep/beep.cpp rename to lv2-examples/beep/beep.cpp index ee22fb6..8837bab 100644 --- a/beep/beep.cpp +++ b/lv2-examples/beep/beep.cpp @@ -1,3 +1,5 @@ +// from: http://ll-plugins.nongnu.org/lv2pftci/#A_synth + #include #include "beep.peg" diff --git a/beep/beep.peg b/lv2-examples/beep/beep.peg similarity index 100% rename from beep/beep.peg rename to lv2-examples/beep/beep.peg diff --git a/beep/beep.ttl b/lv2-examples/beep/beep.ttl similarity index 100% rename from beep/beep.ttl rename to lv2-examples/beep/beep.ttl diff --git a/beep/lv2pftci-beep.lv2/beep.ttl b/lv2-examples/beep/lv2pftci-beep.lv2/beep.ttl similarity index 100% rename from beep/lv2pftci-beep.lv2/beep.ttl rename to lv2-examples/beep/lv2pftci-beep.lv2/beep.ttl diff --git a/beep/lv2pftci-beep.lv2/manifest.ttl b/lv2-examples/beep/lv2pftci-beep.lv2/manifest.ttl similarity index 100% rename from beep/lv2pftci-beep.lv2/manifest.ttl rename to lv2-examples/beep/lv2pftci-beep.lv2/manifest.ttl diff --git a/beep/manifest.ttl b/lv2-examples/beep/manifest.ttl similarity index 100% rename from beep/manifest.ttl rename to lv2-examples/beep/manifest.ttl diff --git a/lv2-examples/eg-sampler.lv2/README.txt b/lv2-examples/eg-sampler.lv2/README.txt new file mode 100644 index 0000000..4eed9e6 --- /dev/null +++ b/lv2-examples/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/lv2-examples/eg-sampler.lv2/atom_sink.h b/lv2-examples/eg-sampler.lv2/atom_sink.h new file mode 100644 index 0000000..ae3df30 --- /dev/null +++ b/lv2-examples/eg-sampler.lv2/atom_sink.h @@ -0,0 +1,40 @@ +/* + Copyright 2016 David Robillard + + 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. +*/ + +/** + A forge sink that writes to an atom buffer. + + It is assumed that the handle points to an LV2_Atom large enough to store + the forge output. The forged result is in the body of the buffer atom. +*/ +static LV2_Atom_Forge_Ref +atom_sink(LV2_Atom_Forge_Sink_Handle handle, const void* buf, uint32_t size) +{ + LV2_Atom* atom = (LV2_Atom*)handle; + const uint32_t offset = lv2_atom_total_size(atom); + memcpy((char*)atom + offset, buf, size); + atom->size += size; + return offset; +} + +/** + Dereference counterpart to atom_sink(). +*/ +static LV2_Atom* +atom_sink_deref(LV2_Atom_Forge_Sink_Handle handle, LV2_Atom_Forge_Ref ref) +{ + return (LV2_Atom*)((char*)handle + ref); +} diff --git a/lv2-examples/eg-sampler.lv2/click.wav b/lv2-examples/eg-sampler.lv2/click.wav new file mode 100644 index 0000000000000000000000000000000000000000..520a18ce6a8b21f398fdd87c75b5e5a758bdfd4c GIT binary patch literal 644 zcmWIYbaSg=Vqge&40BD(Em06)U|?VbLYFlRV9dzC!H|+zk{AJ0(>-POlGU4b9Xx*S z%B}lPU%vbJ^~bNjfB*mg_y6yopFh5QeEah0y<3;h9Y3&Zqa{k1jU0c>Hoinw!t)?_LEiTmG&B4-0 zS6x9yOqh?GgM}Gv1p^Z^8z;Abh=iPyrk<&_vsX}LLS|t_W5>jq^H*%xw(sbvOV{r{ ze*WhDmv2A+K>Pu+QS3^-&T$qoC zla+-L>=8z0Rt|1{VR2bS4P6sUNB4lR`1HK;y7s>5^OvpLvgh#03s>(vc=qc3=dV9O zfd%x+pFcmpfBx|L#iM&SE}l8Mcl-L43uaE}Y^W^AOo$5fcD6Rt(^Qg^5aH+LU}J^` z7BdS67oU)rw1S$pk)^%6UubMfZfR{x@07Vq*KFB!=;XPpw;q5aw$mG}KX5loA)@ . +@prefix rdfs: . +@prefix ui: . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + + + a ui:GtkUI ; + ui:binary ; + rdfs:seeAlso . diff --git a/lv2-examples/eg-sampler.lv2/sampler.c b/lv2-examples/eg-sampler.lv2/sampler.c new file mode 100644 index 0000000..36acb2f --- /dev/null +++ b/lv2-examples/eg-sampler.lv2/sampler.c @@ -0,0 +1,552 @@ +/* + LV2 Sampler Example Plugin + Copyright 2011-2016 David Robillard + Copyright 2011 Gabriel M. Beddingfield + Copyright 2011 James Morris + + 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 +#include +#include +#ifndef __cplusplus +# include +#endif + +#include + +#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 "lv2/lv2plug.in/ns/lv2core/lv2_util.h" + +#include "uris.h" +#include "atom_sink.h" + +enum { + SAMPLER_CONTROL = 0, + SAMPLER_NOTIFY = 1, + SAMPLER_OUT = 2 +}; + +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_Logger logger; + + // Forge for creating atoms + LV2_Atom_Forge forge; + + // 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; + + // Playback state + Sample* sample; + uint32_t frame_offset; + float gain; + sf_count_t frame; + bool play; + bool activated; + bool sample_changed; +} 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(LV2_Log_Logger* logger, const char* path) +{ + lv2_log_trace(logger, "Loading %s\n", path); + + const size_t path_len = strlen(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(logger, "Failed to open sample '%s'\n", path); + free(sample); + return NULL; + } + + // Read data + float* const data = (float*)malloc(sizeof(float) * info->frames); + if (!data) { + lv2_log_error(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 if (atom->type == self->forge.Object) { + // Handle set message (load sample). + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)data; + const char* path = read_set_file(&self->uris, obj); + if (!path) { + lv2_log_error(&self->logger, "Malformed set file request\n"); + return LV2_WORKER_ERR_UNKNOWN; + } + + // Load sample. + Sample* sample = load_sample(&self->logger, path); + if (sample) { + // Send new sample 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; + Sample* old_sample = self->sample; + Sample* new_sample = *(Sample*const*)data; + + // Install the new sample + self->sample = *(Sample*const*)data; + + // Schedule work to free the old sample + SampleMessage msg = { { sizeof(Sample*), self->uris.eg_freeSample }, + old_sample }; + self->schedule->schedule_work(self->schedule->handle, sizeof(msg), &msg); + + if (strcmp(old_sample->path, new_sample->path)) { + // 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, + new_sample->path, + new_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*)calloc(1, sizeof(Sampler)); + if (!self) { + return NULL; + } + + // Get host features + const char* missing = lv2_features_query( + features, + LV2_LOG__log, &self->logger.log, false, + LV2_URID__map, &self->map, true, + LV2_WORKER__schedule, &self->schedule, true, + NULL); + lv2_log_logger_set_map(&self->logger, self->map); + if (missing) { + lv2_log_error(&self->logger, "Missing feature <%s>\n", missing); + free(self); + return NULL; + } + + // Map URIs and initialise forge + map_sampler_uris(self->map, &self->uris); + lv2_atom_forge_init(&self->forge, self->map); + + return (LV2_Handle)self; +} + +static void +cleanup(LV2_Handle instance) +{ + Sampler* self = (Sampler*)instance; + free_sample(self, self->sample); + free(self); +} + +static void +activate(LV2_Handle instance) +{ + ((Sampler*)instance)->activated = true; +} + +static void +deactivate(LV2_Handle instance) +{ + ((Sampler*)instance)->activated = false; +} + +/** Define a macro for converting a gain in dB to a coefficient. */ +#define DB_CO(g) ((g) > -90.0f ? powf(10.0f, (g) * 0.05f) : 0.0f) + +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); + + // Send update to UI if sample has changed due to state restore + if (self->sample_changed) { + lv2_atom_forge_frame_time(&self->forge, 0); + write_set_file(&self->forge, &self->uris, + self->sample->path, + self->sample->path_len); + self->sample_changed = false; + } + + // 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) { + // Get the property and value of the set message + const LV2_Atom* property = NULL; + const LV2_Atom* value = NULL; + lv2_atom_object_get(obj, + uris->patch_property, &property, + uris->patch_value, &value, + 0); + if (!property) { + lv2_log_error(&self->logger, + "patch:Set message with no property\n"); + continue; + } else if (property->type != uris->atom_URID) { + lv2_log_error(&self->logger, + "patch:Set property is not a URID\n"); + continue; + } + + const uint32_t key = ((const LV2_Atom_URID*)property)->body; + if (key == uris->eg_sample) { + // Sample change, send it to the worker. + lv2_log_trace(&self->logger, "Scheduling sample change\n"); + self->schedule->schedule_work(self->schedule->handle, + lv2_atom_total_size(&ev->body), + &ev->body); + } else if (key == uris->param_gain) { + // Gain change + if (value->type == uris->atom_Float) { + self->gain = DB_CO(((LV2_Atom_Float*)value)->body); + } + } + } else if (obj->body.otype == uris->patch_Get) { + // Received a get message, emit our state (probably to UI) + lv2_atom_forge_frame_time(&self->forge, self->frame_offset); + write_set_file(&self->forge, &self->uris, + self->sample->path, + self->sample->path_len); + } 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->gain; + } + + 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 = (LV2_State_Map_Path*)lv2_features_data( + features, LV2_STATE__mapPath); + if (!map_path) { + return LV2_STATE_ERR_NO_FEATURE; + } + + // Map absolute sample path to an abstract state path + char* apath = map_path->abstract_path(map_path->handle, self->sample->path); + + // Store eg:sample = abstract path + store(handle, + self->uris.eg_sample, + apath, + strlen(apath) + 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; + + // Get host features + LV2_Worker_Schedule* schedule = NULL; + LV2_State_Map_Path* paths = NULL; + const char* missing = lv2_features_query( + features, + LV2_STATE__mapPath, &paths, true, + LV2_WORKER__schedule, &schedule, false, + NULL); + if (missing) { + lv2_log_error(&self->logger, "Missing feature <%s>\n", missing); + return LV2_STATE_ERR_NO_FEATURE; + } + + // Get eg:sample from state + size_t size; + uint32_t type; + uint32_t valflags; + const void* value = retrieve(handle, self->uris.eg_sample, + &size, &type, &valflags); + if (!value) { + lv2_log_error(&self->logger, "Missing eg:sample\n"); + return LV2_STATE_ERR_NO_PROPERTY; + } else if (type != self->uris.atom_Path) { + lv2_log_error(&self->logger, "Non-path eg:sample\n"); + return LV2_STATE_ERR_BAD_TYPE; + } + + // Map abstract state path to absolute path + const char* apath = (const char*)value; + char* path = paths->absolute_path(paths->handle, apath); + + // Replace current sample with the new one + if (!self->activated || !schedule) { + // No scheduling available, load sample immediately + lv2_log_trace(&self->logger, "Synchronous restore\n"); + free_sample(self, self->sample); + self->sample = load_sample(&self->logger, path); + self->sample_changed = true; + } else { + // Schedule sample to be loaded by the provided worker + lv2_log_trace(&self->logger, "Scheduling restore\n"); + LV2_Atom_Forge forge; + LV2_Atom* buf = (LV2_Atom*)calloc(1, strlen(path) + 128); + lv2_atom_forge_init(&forge, self->map); + lv2_atom_forge_set_sink(&forge, atom_sink, atom_sink_deref, buf); + write_set_file(&forge, &self->uris, path, strlen(path)); + + const uint32_t msg_size = lv2_atom_pad_size(buf->size); + schedule->schedule_work(self->schedule->handle, msg_size, buf + 1); + free(buf); + } + + 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, + activate, + run, + 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/lv2-examples/eg-sampler.lv2/sampler.ttl b/lv2-examples/eg-sampler.lv2/sampler.ttl new file mode 100644 index 0000000..f4a9c43 --- /dev/null +++ b/lv2-examples/eg-sampler.lv2/sampler.ttl @@ -0,0 +1,70 @@ +@prefix atom: . +@prefix doap: . +@prefix lv2: . +@prefix patch: . +@prefix rdfs: . +@prefix state: . +@prefix ui: . +@prefix urid: . +@prefix work: . +@prefix param: . + + + a lv2:Parameter ; + rdfs:label "sample" ; + rdfs:range atom:Path . + + + a lv2:Plugin ; + doap:name "Example Sampler" ; + doap:license ; + lv2:project ; + lv2:requiredFeature state:loadDefaultState , + urid:map , + work:schedule ; + lv2:optionalFeature lv2:hardRTCapable , + state:threadSafeRestore ; + lv2:extensionData state:interface , + work:interface ; + ui:ui ; + patch:writable , + param:gain ; + lv2:port [ + a lv2:InputPort , + atom:AtomPort ; + atom:bufferType atom:Sequence ; + atom:supports , + 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 [ + + ] . + + + a ui:GtkUI ; + lv2:requiredFeature urid:map ; + lv2:extensionData ui:showInterface ; + ui:portNotification [ + ui:plugin ; + lv2:symbol "notify" ; + ui:notifyType atom:Blank + ] . diff --git a/lv2-examples/eg-sampler.lv2/sampler_ui.c b/lv2-examples/eg-sampler.lv2/sampler_ui.c new file mode 100644 index 0000000..23204c5 --- /dev/null +++ b/lv2-examples/eg-sampler.lv2/sampler_ui.c @@ -0,0 +1,254 @@ +/* + LV2 Sampler Example Plugin UI + Copyright 2011-2016 David Robillard + + 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 + +#include + +#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/log/logger.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 "lv2/lv2plug.in/ns/lv2core/lv2_util.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; + LV2_Log_Logger logger; + 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 = (LV2_Atom*)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*)calloc(1, sizeof(SamplerUI)); + if (!ui) { + return NULL; + } + + ui->write = write_function; + ui->controller = controller; + *widget = NULL; + + // Get host features + const char* missing = lv2_features_query( + features, + LV2_LOG__log, &ui->logger.log, false, + LV2_URID__map, &ui->map, true, + NULL); + lv2_log_logger_set_map(&ui->logger, ui->map); + if (missing) { + lv2_log_error(&ui->logger, "Missing feature <%s>\n", missing); + free(ui); + return NULL; + } + + // Map URIs and initialise forge + map_sampler_uris(ui->map, &ui->uris); + lv2_atom_forge_init(&ui->forge, ui->map); + + // Construct Gtk UI + 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); + + // Request state (filename) from plugin + uint8_t get_buf[512]; + lv2_atom_forge_set_buffer(&ui->forge, get_buf, sizeof(get_buf)); + + LV2_Atom_Forge_Frame frame; + LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object( + &ui->forge, &frame, 0, ui->uris.patch_Get); + lv2_atom_forge_pop(&ui->forge, &frame); + + ui->write(ui->controller, 0, lv2_atom_total_size(msg), + ui->uris.atom_eventTransfer, + msg); + + *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 (lv2_atom_forge_is_object_type(&ui->forge, atom->type)) { + const LV2_Atom_Object* obj = (const LV2_Atom_Object*)atom; + const char* uri = read_set_file(&ui->uris, obj); + if (uri) { + gtk_label_set_text(GTK_LABEL(ui->label), uri); + } else { + lv2_log_warning(&ui->logger, "Malformed message\n"); + } + } else { + lv2_log_error(&ui->logger, "Unknown message type\n"); + } + } else { + lv2_log_warning(&ui->logger, "Unknown port event 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/lv2-examples/eg-sampler.lv2/uris.h b/lv2-examples/eg-sampler.lv2/uris.h new file mode 100644 index 0000000..ceeddc0 --- /dev/null +++ b/lv2-examples/eg-sampler.lv2/uris.h @@ -0,0 +1,139 @@ +/* + LV2 Sampler Example Plugin + Copyright 2011-2016 David Robillard + + 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" +#include "lv2/lv2plug.in/ns/ext/parameters/parameters.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_Float; + 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 param_gain; + LV2_URID patch_Get; + 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_Float = map->map(map->handle, LV2_ATOM__Float); + 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->param_gain = map->map(map->handle, LV2_PARAMETERS__gain); + uris->patch_Get = map->map(map->handle, LV2_PATCH__Get); + 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 . + */ +static inline LV2_Atom_Forge_Ref +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_Forge_Ref set = 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 . + */ +static inline const char* +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* value = NULL; + lv2_atom_object_get(obj, uris->patch_value, &value, 0); + if (!value) { + fprintf(stderr, "Malformed set message has no value.\n"); + return NULL; + } else if (value->type != uris->atom_Path) { + fprintf(stderr, "Set message value is not a Path.\n"); + return NULL; + } + + return (const char*)LV2_ATOM_BODY_CONST(value); +} + +#endif /* SAMPLER_URIS_H */ diff --git a/lv2-examples/eg-sampler.lv2/waf b/lv2-examples/eg-sampler.lv2/waf new file mode 120000 index 0000000..59a1ac9 --- /dev/null +++ b/lv2-examples/eg-sampler.lv2/waf @@ -0,0 +1 @@ +../../waf \ No newline at end of file diff --git a/lv2-examples/eg-sampler.lv2/wscript b/lv2-examples/eg-sampler.lv2/wscript new file mode 100644 index 0000000..3032953 --- /dev/null +++ b/lv2-examples/eg-sampler.lv2/wscript @@ -0,0 +1,83 @@ +#!/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') + opt.load('lv2') + autowaf.set_options(opt) + +def configure(conf): + conf.load('compiler_c') + conf.load('lv2') + 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) + conf.check(features='c cshlib', lib='m', uselib_store='M', 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 = ['M', 'SNDFILE', 'LV2'], + includes = includes) + obj.env.cshlib_PATTERN = module_pat + + # Build UI library + if bld.env.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 diff --git a/src/.Makefile.swp b/src/.Makefile.swp deleted file mode 100644 index 6b3f35f36399e5e1a54c6f4a87b8f566f09e3ce6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI1O=}ZT6oyaTbx{>{A+D}%0=8xHQH7LXK2lkork&k2?XGb;t{J_Z2^ye* zlN#tHjx|3a*XOF0{)nl`tLox~&67^!J{q6_8lV9hpaB}70UDrzQ)nOw$K(mleP(#} zE5mi{uXR{*p#d780UDqI8lV9hpaB}70UDqI8aRap?6%0436X=dI6VIU|NZ;_<2jKJ z;5~Q)j=(GM96SXnu)!L*1*%{YTmnBa>pS=cK7)7QEjR)%!3*#V^q;u{R>3?dfGNO> z255i=Xn+Q2fCgxQ255i=XyE@C7%yB~*xsI2SeEbJUfGyd*4DA!=!VK7$8s2EyRNdd zD%_M}DX6PrNr$1xzms0<*ySufywe(6B2QV4nkfZp<`B!S-Noyk-WTl}+pi