diff options
Diffstat (limited to 'plugins/eg-sampler.lv2')
-rw-r--r-- | plugins/eg-sampler.lv2/README.txt | 14 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/atom_sink.h | 42 | ||||
-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/peaks.h | 271 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/sampler.c | 603 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/sampler.ttl | 71 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/sampler_ui.c | 393 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/uris.h | 150 | ||||
l--------- | plugins/eg-sampler.lv2/waf | 1 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/wscript | 69 |
11 files changed, 1633 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..8d136fa --- /dev/null +++ b/plugins/eg-sampler.lv2/README.txt @@ -0,0 +1,14 @@ +== 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 +- Network-transparent waveform display with incremental peak transmission diff --git a/plugins/eg-sampler.lv2/atom_sink.h b/plugins/eg-sampler.lv2/atom_sink.h new file mode 100644 index 0000000..b84ca55 --- /dev/null +++ b/plugins/eg-sampler.lv2/atom_sink.h @@ -0,0 +1,42 @@ +/* + Copyright 2016 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 "lv2/atom/forge.h" + +/** + 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/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/peaks.h b/plugins/eg-sampler.lv2/peaks.h new file mode 100644 index 0000000..a5bd267 --- /dev/null +++ b/plugins/eg-sampler.lv2/peaks.h @@ -0,0 +1,271 @@ +/* + LV2 audio peaks utilities + Copyright 2016 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. +*/ + +/** + This file defines utilities for sending and receiving audio peaks for + waveform display. The functionality is divided into two objects: + PeaksSender, for sending peaks updates from the plugin, and PeaksReceiver, + for receiving such updates and caching the peaks. + + This allows peaks for a waveform of any size at any resolution to be + requested, with reasonably sized incremental updates sent over plugin ports. +*/ + +#ifndef PEAKS_H_INCLUDED +#define PEAKS_H_INCLUDED + +#include "lv2/atom/forge.h" + +#include <math.h> +#include <stdlib.h> + +#define PEAKS_URI "http://lv2plug.in/ns/peaks#" +#define PEAKS__PeakUpdate PEAKS_URI "PeakUpdate" +#define PEAKS__magnitudes PEAKS_URI "magnitudes" +#define PEAKS__offset PEAKS_URI "offset" +#define PEAKS__total PEAKS_URI "total" + +#ifndef MIN +# define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif +#ifndef MAX +# define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#endif + +typedef struct { + LV2_URID atom_Float; + LV2_URID atom_Int; + LV2_URID atom_Vector; + LV2_URID peaks_PeakUpdate; + LV2_URID peaks_magnitudes; + LV2_URID peaks_offset; + LV2_URID peaks_total; +} PeaksURIs; + +typedef struct { + PeaksURIs uris; ///< URIDs used in protocol + const float* samples; ///< Sample data + uint32_t n_samples; ///< Total number of samples + uint32_t n_peaks; ///< Total number of peaks + uint32_t current_offset; ///< Current peak offset + bool sending; ///< True iff currently sending +} PeaksSender; + +typedef struct { + PeaksURIs uris; ///< URIDs used in protocol + float* peaks; ///< Received peaks, or zeroes + uint32_t n_peaks; ///< Total number of peaks +} PeaksReceiver; + +/** + Map URIs used in the peaks protocol. +*/ +static inline void +peaks_map_uris(PeaksURIs* uris, LV2_URID_Map* map) +{ + uris->atom_Float = map->map(map->handle, LV2_ATOM__Float); + uris->atom_Int = map->map(map->handle, LV2_ATOM__Int); + uris->atom_Vector = map->map(map->handle, LV2_ATOM__Vector); + uris->peaks_PeakUpdate = map->map(map->handle, PEAKS__PeakUpdate); + uris->peaks_magnitudes = map->map(map->handle, PEAKS__magnitudes); + uris->peaks_offset = map->map(map->handle, PEAKS__offset); + uris->peaks_total = map->map(map->handle, PEAKS__total); +} + +/** + Initialise peaks sender. The new sender is inactive and will do nothing + when `peaks_sender_send()` is called, until a transmission is started with + `peaks_sender_start()`. +*/ +static inline PeaksSender* +peaks_sender_init(PeaksSender* sender, LV2_URID_Map* map) +{ + memset(sender, 0, sizeof(*sender)); + peaks_map_uris(&sender->uris, map); + return sender; +} + +/** + Prepare to start a new peaks transmission. After this is called, the peaks + can be sent with successive calls to `peaks_sender_send()`. +*/ +static inline void +peaks_sender_start(PeaksSender* sender, + const float* samples, + uint32_t n_samples, + uint32_t n_peaks) +{ + sender->samples = samples; + sender->n_samples = n_samples; + sender->n_peaks = n_peaks; + sender->current_offset = 0; + sender->sending = true; +} + +/** + Forge a message which sends a range of peaks. Writes a peaks:PeakUpdate + object to `forge`, like: + + [source,n3] + ---- + [] + a peaks:PeakUpdate ; + peaks:offset 256 ; + peaks:total 1024 ; + peaks:magnitudes [ 0.2f, 0.3f, ... ] . + ---- +*/ +static inline bool +peaks_sender_send(PeaksSender* sender, + LV2_Atom_Forge* forge, + uint32_t n_frames, + uint32_t offset) +{ + const PeaksURIs* uris = &sender->uris; + if (!sender->sending || sender->current_offset >= sender->n_peaks) { + return sender->sending = false; + } + + // Start PeakUpdate object + lv2_atom_forge_frame_time(forge, offset); + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_object(forge, &frame, 0, uris->peaks_PeakUpdate); + + // eg:offset = OFFSET + lv2_atom_forge_key(forge, uris->peaks_offset); + lv2_atom_forge_int(forge, sender->current_offset); + + // eg:total = TOTAL + lv2_atom_forge_key(forge, uris->peaks_total); + lv2_atom_forge_int(forge, sender->n_peaks); + + // eg:magnitudes = Vector<Float>(PEAK, PEAK, ...) + lv2_atom_forge_key(forge, uris->peaks_magnitudes); + LV2_Atom_Forge_Frame vec_frame; + lv2_atom_forge_vector_head( + forge, &vec_frame, sizeof(float), uris->atom_Float); + + // Calculate how many peaks to send this update + const int chunk_size = MAX(1, sender->n_samples / sender->n_peaks); + const uint32_t space = forge->size - forge->offset; + const uint32_t remaining = sender->n_peaks - sender->current_offset; + const int n_update = MIN(remaining, + MIN(n_frames / 4, space / sizeof(float))); + + // Calculate peak (maximum magnitude) for each chunk + for (int i = 0; i < n_update; ++i) { + const int start = (sender->current_offset + i) * chunk_size; + float peak = 0.0f; + for (int j = 0; j < chunk_size; ++j) { + peak = fmaxf(peak, fabsf(sender->samples[start + j])); + } + lv2_atom_forge_float(forge, peak); + } + + // Finish message + lv2_atom_forge_pop(forge, &vec_frame); + lv2_atom_forge_pop(forge, &frame); + + sender->current_offset += n_update; + return true; +} + +/** + Initialise a peaks receiver. The receiver stores an array of all peaks, + which is updated incrementally with peaks_receiver_receive(). +*/ +static inline PeaksReceiver* +peaks_receiver_init(PeaksReceiver* receiver, LV2_URID_Map* map) +{ + memset(receiver, 0, sizeof(*receiver)); + peaks_map_uris(&receiver->uris, map); + return receiver; +} + +/** + Clear stored peaks and free all memory. This should be called when the + peaks are to be updated with a different audio source. +*/ +static inline void +peaks_receiver_clear(PeaksReceiver* receiver) +{ + free(receiver->peaks); + receiver->peaks = NULL; + receiver->n_peaks = 0; +} + +/** + Handle PeakUpdate message. + + The stored peaks array is updated with the slice of peaks in `update`, + resizing if necessary while preserving contents. + + Returns 0 if peaks have been updated, negative on error. +*/ +static inline int +peaks_receiver_receive(PeaksReceiver* receiver, const LV2_Atom_Object* update) +{ + const PeaksURIs* uris = &receiver->uris; + + // Get properties of interest from update + const LV2_Atom_Int* offset = NULL; + const LV2_Atom_Int* total = NULL; + const LV2_Atom_Vector* peaks = NULL; + lv2_atom_object_get_typed(update, + uris->peaks_offset, &offset, uris->atom_Int, + uris->peaks_total, &total, uris->atom_Int, + uris->peaks_magnitudes, &peaks, uris->atom_Vector, + 0); + + if (!offset || !total || !peaks || + peaks->body.child_type != uris->atom_Float) { + return -1; // Invalid update + } + + const uint32_t n = (uint32_t)total->body; + if (receiver->n_peaks != n) { + // Update is for a different total number of peaks, resize + receiver->peaks = (float*)realloc(receiver->peaks, n * sizeof(float)); + if (receiver->n_peaks > 0 && receiver->n_peaks < n) { + /* The peaks array is being expanded. Copy the old peaks, + duplicating each as necessary to fill the new peaks buffer. + This preserves the current peaks so that the peaks array can be + reasonably drawn at any time, but the resolution will increase + as new updates arrive. */ + const int n_per = n / receiver->n_peaks; + for (int i = n - 1; i >= 0; --i) { + receiver->peaks[i] = receiver->peaks[i / n_per]; + } + } else if (receiver->n_peaks > 0) { + /* The peak array is being shrunk. Similar to the above. */ + const int n_per = receiver->n_peaks / n; + for (int i = n - 1; i >= 0; --i) { + receiver->peaks[i] = receiver->peaks[i * n_per]; + } + } + receiver->n_peaks = n; + } + + // Copy vector contents to corresponding range in peaks array + memcpy(receiver->peaks + offset->body, + peaks + 1, + peaks->atom.size - sizeof(LV2_Atom_Vector_Body)); + + return 0; +} + +#endif // PEAKS_H_INCLUDED diff --git a/plugins/eg-sampler.lv2/sampler.c b/plugins/eg-sampler.lv2/sampler.c new file mode 100644 index 0000000..4e6afdf --- /dev/null +++ b/plugins/eg-sampler.lv2/sampler.c @@ -0,0 +1,603 @@ +/* + LV2 Sampler Example Plugin + Copyright 2011-2016 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 "atom_sink.h" +#include "peaks.h" +#include "uris.h" + +#include "lv2/atom/atom.h" +#include "lv2/atom/forge.h" +#include "lv2/atom/util.h" +#include "lv2/core/lv2.h" +#include "lv2/core/lv2_util.h" +#include "lv2/log/log.h" +#include "lv2/log/logger.h" +#include "lv2/midi/midi.h" +#include "lv2/state/state.h" +#include "lv2/urid/urid.h" +#include "lv2/worker/worker.h" + +#include <sndfile.h> + +#include <math.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.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; + + // Ports + const LV2_Atom_Sequence* control_port; + LV2_Atom_Sequence* notify_port; + float* output_port; + + // Communication utilities + LV2_Atom_Forge_Frame notify_frame; ///< Cached for worker replies + LV2_Atom_Forge forge; ///< Forge for writing atoms in run thread + PeaksSender psend; ///< Audio peaks sender + + // 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*)calloc(1, sizeof(Sample)); + SF_INFO* const info = &sample->info; + SNDFILE* const sndfile = sf_open(path, SFM_READ, info); + float* data = NULL; + bool error = true; + if (!sndfile || !info->frames) { + lv2_log_error(logger, "Failed to open %s\n", path); + } else if (info->channels != 1) { + lv2_log_error(logger, "%s has %d channels\n", path, info->channels); + } else if (!(data = (float*)malloc(sizeof(float) * info->frames))) { + lv2_log_error(logger, "Failed to allocate memory for sample\n"); + } else { + error = false; + } + + if (error) { + free(sample); + free(data); + sf_close(sndfile); + 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); + + // 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); + peaks_sender_init(&self->psend, self->map); + + self->gain = 1.0; + + 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) + +/** + Handle an incoming event in the audio thread. + + This performs any actions triggered by an event, such as the start of sample + playback, a sample change, or responding to requests from the UI. +*/ +static void +handle_event(Sampler* self, LV2_Atom_Event* ev) +{ + SamplerURIs* uris = &self->uris; + PeaksURIs* peaks_uris = &self->psend.uris; + + 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: + 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, "Set message with no property\n"); + return; + } else if (property->type != uris->atom_URID) { + lv2_log_error(&self->logger, "Set property is not a URID\n"); + return; + } + + 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 && self->sample) { + const LV2_Atom_URID* accept = NULL; + const LV2_Atom_Int* n_peaks = NULL; + lv2_atom_object_get_typed( + obj, + uris->patch_accept, &accept, uris->atom_URID, + peaks_uris->peaks_total, &n_peaks, peaks_uris->atom_Int, 0); + if (accept && accept->body == peaks_uris->peaks_PeakUpdate) { + // Received a request for peaks, prepare for transmission + peaks_sender_start(&self->psend, + self->sample->data, + self->sample->info.frames, + n_peaks->body); + } else { + // 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); + } + +} + +/** + Output audio for a slice of the current cycle. +*/ +static void +render(Sampler* self, uint32_t start, uint32_t end) +{ + float* output = self->output_port; + + if (self->play && self->sample) { + // Start/continue writing sample to output + for (; start < end; ++start) { + output[start] = self->sample->data[self->frame] * self->gain; + if (++self->frame == self->sample->info.frames) { + self->play = false; // Reached end of sample + break; + } + } + } + + // Write silence to remaining buffer + for (; start < end; ++start) { + output[start] = 0.0f; + } +} + +static void +run(LV2_Handle instance, uint32_t sample_count) +{ + Sampler* self = (Sampler*)instance; + + // 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; + } + + // Iterate over incoming events, emitting audio along the way + self->frame_offset = 0; + LV2_ATOM_SEQUENCE_FOREACH(self->control_port, ev) { + // Render output up to the time of this event + render(self, self->frame_offset, ev->time.frames); + + /* Update current frame offset to this event's time. This is stored in + the instance because it is used for sychronous worker event + execution. This allows a sample load event to be executed with + sample accuracy when running in a non-realtime context (such as + exporting a session). */ + self->frame_offset = ev->time.frames; + + // Process this event + handle_event(self, ev); + } + + // Use available space after any emitted events to send peaks + peaks_sender_send(&self->psend, &self->forge, sample_count, self->frame_offset); + + // Render output for the rest of the cycle past the last event + render(self, self->frame_offset, sample_count); +} + +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"); + Sample* sample = load_sample(&self->logger, path); + if (sample) { + free_sample(self, self->sample); + self->sample = sample; + 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); + } + + free(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, + 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/plugins/eg-sampler.lv2/sampler.ttl b/plugins/eg-sampler.lv2/sampler.ttl new file mode 100644 index 0000000..92570e5 --- /dev/null +++ b/plugins/eg-sampler.lv2/sampler.ttl @@ -0,0 +1,71 @@ +@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#> . +@prefix param: <http://lv2plug.in/ns/ext/parameters#> . + +<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 "Exampler" ; + doap:license <http://opensource.org/licenses/isc> ; + lv2:project <http://lv2plug.in/ns/lv2> ; + lv2:requiredFeature state:loadDefaultState , + urid:map , + work:schedule ; + lv2:optionalFeature lv2:hardRTCapable , + state:threadSafeRestore ; + lv2:extensionData state:interface , + work:interface ; + ui:ui <http://lv2plug.in/plugins/eg-sampler#ui> ; + patch:writable <http://lv2plug.in/plugins/eg-sampler#sample> , + param:gain ; + 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> ; + param:gain 1.0 + ] . + +<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..89bca2f --- /dev/null +++ b/plugins/eg-sampler.lv2/sampler_ui.c @@ -0,0 +1,393 @@ +/* + LV2 Sampler Example Plugin UI + Copyright 2011-2016 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 "peaks.h" +#include "uris.h" + +#include "lv2/atom/atom.h" +#include "lv2/atom/forge.h" +#include "lv2/atom/util.h" +#include "lv2/core/lv2.h" +#include "lv2/core/lv2_util.h" +#include "lv2/log/log.h" +#include "lv2/log/logger.h" +#include "lv2/midi/midi.h" +#include "lv2/ui/ui.h" +#include "lv2/urid/urid.h" + +#include <cairo.h> +#include <gdk/gdk.h> +#include <glib-object.h> +#include <glib.h> +#include <gobject/gclosure.h> +#include <gtk/gtk.h> + +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#define SAMPLER_UI_URI "http://lv2plug.in/plugins/eg-sampler#ui" + +#define MIN_CANVAS_W 128 +#define MIN_CANVAS_H 80 + +typedef struct { + LV2_Atom_Forge forge; + LV2_URID_Map* map; + LV2_Log_Logger logger; + SamplerURIs uris; + PeaksReceiver precv; + + LV2UI_Write_Function write; + LV2UI_Controller controller; + + GtkWidget* box; + GtkWidget* play_button; + GtkWidget* file_button; + GtkWidget* button_box; + GtkWidget* canvas; + GtkWidget* window; /* For optional show interface. */ + + uint32_t width; + uint32_t requested_n_peaks; + char* filename; + + uint8_t forge_buf[1024]; +} SamplerUI; + +static void +on_file_set(GtkFileChooserButton* widget, void* handle) +{ + SamplerUI* ui = (SamplerUI*)handle; + + // Get the filename from the file chooser + char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); + + // Write a set message to the plugin to load new file + lv2_atom_forge_set_buffer(&ui->forge, ui->forge_buf, sizeof(ui->forge_buf)); + 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 void +on_play_clicked(GtkFileChooserButton* widget, void* handle) +{ + SamplerUI* ui = (SamplerUI*)handle; + struct { + LV2_Atom atom; + uint8_t msg[3]; + } note_on; + + note_on.atom.type = ui->uris.midi_Event; + note_on.atom.size = 3; + note_on.msg[0] = LV2_MIDI_MSG_NOTE_ON; + note_on.msg[1] = 60; + note_on.msg[2] = 60; + ui->write(ui->controller, 0, sizeof(note_on), + ui->uris.atom_eventTransfer, + ¬e_on); +} + +static void +request_peaks(SamplerUI* ui, uint32_t n_peaks) +{ + if (n_peaks == ui->requested_n_peaks) { + return; + } + + lv2_atom_forge_set_buffer(&ui->forge, ui->forge_buf, sizeof(ui->forge_buf)); + + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_object(&ui->forge, &frame, 0, ui->uris.patch_Get); + lv2_atom_forge_key(&ui->forge, ui->uris.patch_accept); + lv2_atom_forge_urid(&ui->forge, ui->precv.uris.peaks_PeakUpdate); + lv2_atom_forge_key(&ui->forge, ui->precv.uris.peaks_total); + lv2_atom_forge_int(&ui->forge, n_peaks); + lv2_atom_forge_pop(&ui->forge, &frame); + + LV2_Atom* msg = lv2_atom_forge_deref(&ui->forge, frame.ref); + ui->write(ui->controller, 0, lv2_atom_total_size(msg), + ui->uris.atom_eventTransfer, + msg); + + ui->requested_n_peaks = n_peaks; +} + +/** Set Cairo color to a GDK color (to follow Gtk theme). */ +static void +cairo_set_source_gdk(cairo_t* cr, const GdkColor* color) +{ + cairo_set_source_rgb( + cr, color->red / 65535.0, color->green / 65535.0, color->blue / 65535.0); + +} + +static gboolean +on_canvas_expose(GtkWidget* widget, GdkEventExpose* event, gpointer data) +{ + SamplerUI* ui = (SamplerUI*)data; + + GtkAllocation size; + gtk_widget_get_allocation(widget, &size); + + ui->width = size.width; + if ((uint32_t)ui->width > 2 * ui->requested_n_peaks) { + request_peaks(ui, 2 * ui->requested_n_peaks); + } + + cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(widget)); + + cairo_set_line_width(cr, 1.0); + cairo_translate(cr, 0.5, 0.5); + + const int mid_y = size.height / 2; + + const float* const peaks = ui->precv.peaks; + const int32_t n_peaks = ui->precv.n_peaks; + if (peaks) { + // Draw waveform + const double scale = size.width / ((double)n_peaks - 1.0f); + + // Start at left origin + cairo_move_to(cr, 0, mid_y); + + // Draw line through top peaks + for (int i = 0; i < n_peaks; ++i) { + const float peak = peaks[i]; + cairo_line_to(cr, i * scale, mid_y + (peak / 2.0f) * size.height); + } + + // Continue through bottom peaks + for (int i = n_peaks - 1; i >= 0; --i) { + const float peak = peaks[i]; + cairo_line_to(cr, i * scale, mid_y - (peak / 2.0f) * size.height); + } + + // Close shape + cairo_line_to(cr, 0, mid_y); + + cairo_set_source_gdk(cr, widget->style->mid); + cairo_fill_preserve(cr); + + cairo_set_source_gdk(cr, widget->style->fg); + cairo_stroke(cr); + } + + cairo_destroy(cr); + return TRUE; +} + +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; + ui->width = MIN_CANVAS_W; + *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); + peaks_receiver_init(&ui->precv, ui->map); + + // Construct Gtk UI + ui->box = gtk_vbox_new(FALSE, 4); + ui->play_button = gtk_button_new_with_label("▶"); + ui->canvas = gtk_drawing_area_new(); + ui->button_box = gtk_hbox_new(FALSE, 4); + ui->file_button = gtk_file_chooser_button_new( + "Load Sample", GTK_FILE_CHOOSER_ACTION_OPEN); + gtk_widget_set_size_request(ui->canvas, MIN_CANVAS_W, MIN_CANVAS_H); + gtk_container_set_border_width(GTK_CONTAINER(ui->box), 4); + gtk_box_pack_start(GTK_BOX(ui->box), ui->canvas, TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(ui->box), ui->button_box, FALSE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(ui->button_box), ui->play_button, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(ui->button_box), ui->file_button, TRUE, TRUE, 0); + + g_signal_connect(ui->file_button, "file-set", + G_CALLBACK(on_file_set), ui); + + g_signal_connect(ui->play_button, "clicked", + G_CALLBACK(on_play_clicked), ui); + + g_signal_connect(G_OBJECT(ui->canvas), "expose_event", + G_CALLBACK(on_canvas_expose), ui); + + // Request state (filename) from plugin + lv2_atom_forge_set_buffer(&ui->forge, ui->forge_buf, sizeof(ui->forge_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->box); + gtk_widget_destroy(ui->play_button); + gtk_widget_destroy(ui->canvas); + gtk_widget_destroy(ui->button_box); + gtk_widget_destroy(ui->file_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; + if (obj->body.otype == ui->uris.patch_Set) { + const char* path = read_set_file(&ui->uris, obj); + if (path && (!ui->filename || strcmp(path, ui->filename))) { + g_free(ui->filename); + ui->filename = g_strdup(path); + gtk_file_chooser_set_filename( + GTK_FILE_CHOOSER(ui->file_button), path); + peaks_receiver_clear(&ui->precv); + ui->requested_n_peaks = 0; + request_peaks(ui, ui->width / 2 * 2); + } else if (!path) { + lv2_log_warning(&ui->logger, "Set message has no path\n"); + } + } else if (obj->body.otype == ui->precv.uris.peaks_PeakUpdate) { + if (!peaks_receiver_receive(&ui->precv, obj)) { + gtk_widget_queue_draw(ui->canvas); + } + } + } 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/plugins/eg-sampler.lv2/uris.h b/plugins/eg-sampler.lv2/uris.h new file mode 100644 index 0000000..b3639bc --- /dev/null +++ b/plugins/eg-sampler.lv2/uris.h @@ -0,0 +1,150 @@ +/* + LV2 Sampler Example Plugin + Copyright 2011-2016 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/log/log.h" +#include "lv2/midi/midi.h" +#include "lv2/parameters/parameters.h" +#include "lv2/patch/patch.h" +#include "lv2/state/state.h" + +#include <stdio.h> + +#define EG_SAMPLER_URI "http://lv2plug.in/plugins/eg-sampler" +#define EG_SAMPLER__applySample EG_SAMPLER_URI "#applySample" +#define EG_SAMPLER__freeSample EG_SAMPLER_URI "#freeSample" +#define EG_SAMPLER__sample EG_SAMPLER_URI "#sample" + +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_freeSample; + LV2_URID eg_sample; + LV2_URID midi_Event; + LV2_URID param_gain; + LV2_URID patch_Get; + LV2_URID patch_Set; + LV2_URID patch_accept; + 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_accept = map->map(map->handle, LV2_PATCH__accept); + 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 `forge`: + [source,n3] + ---- + [] + a patch:Set ; + patch:property eg:sample ; + patch:value </home/me/foo.wav> . + ---- +*/ +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 `obj` which is a message like: + [source,n3] + ---- + [] + a patch:Set ; + patch:property eg:sample ; + patch:value </home/me/foo.wav> . + ---- +*/ +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/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..7e33d68 --- /dev/null +++ b/plugins/eg-sampler.lv2/wscript @@ -0,0 +1,69 @@ +#!/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', cache=True) + conf.load('lv2', cache=True) + conf.load('autowaf', cache=True) + + 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) + +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) + + # 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']) + 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']) + obj.env.cshlib_PATTERN = module_pat |