diff options
author | David Robillard <d@drobilla.net> | 2016-10-04 00:46:56 -0400 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2016-10-04 00:47:20 -0400 |
commit | 472556ff28b99db416d6f0a926c32ea28698e1a3 (patch) | |
tree | e115542762a966d0303c8f226f5984d5fb378ac9 | |
parent | 2813b3f9f6a778641b3ff409a43e969cd8a8fc20 (diff) | |
download | lv2-472556ff28b99db416d6f0a926c32ea28698e1a3.tar.xz |
eg-sampler: Add waveform display to UI
-rw-r--r-- | lv2/lv2plug.in/ns/meta/meta.ttl | 9 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/README.txt | 1 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/peaks.h | 270 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/sampler.c | 64 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/sampler.ttl | 2 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/sampler_ui.c | 169 | ||||
-rw-r--r-- | plugins/eg-sampler.lv2/uris.h | 36 |
7 files changed, 475 insertions, 76 deletions
diff --git a/lv2/lv2plug.in/ns/meta/meta.ttl b/lv2/lv2plug.in/ns/meta/meta.ttl index e3ded9a..caf5f0e 100644 --- a/lv2/lv2plug.in/ns/meta/meta.ttl +++ b/lv2/lv2plug.in/ns/meta/meta.ttl @@ -46,6 +46,15 @@ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH R meta:kfoltman , meta:paniq ; doap:release [ + doap:revision "1.14.1" ; + doap:created "2016-10-04" ; + dcs:blame <http://drobilla.net/drobilla#me> ; + dcs:changeset [ + dcs:item [ + rdfs:label "eg-sampler: Add waveform display to UI." + ] + ] + ] , [ doap:revision "1.14.0" ; doap:created "2016-09-19" ; doap:file-release <http://lv2plug.in/spec/lv2-1.14.0.tar.bz2> ; diff --git a/plugins/eg-sampler.lv2/README.txt b/plugins/eg-sampler.lv2/README.txt index 4eed9e6..8d136fa 100644 --- a/plugins/eg-sampler.lv2/README.txt +++ b/plugins/eg-sampler.lv2/README.txt @@ -11,3 +11,4 @@ This plugin illustrates: - 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/peaks.h b/plugins/eg-sampler.lv2/peaks.h new file mode 100644 index 0000000..69688b8 --- /dev/null +++ b/plugins/eg-sampler.lv2/peaks.h @@ -0,0 +1,270 @@ +/* + 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 <math.h> + +#include "lv2/lv2plug.in/ns/ext/atom/forge.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 index 36acb2f..627ba45 100644 --- a/plugins/eg-sampler.lv2/sampler.c +++ b/plugins/eg-sampler.lv2/sampler.c @@ -38,8 +38,9 @@ #include "lv2/lv2plug.in/ns/lv2core/lv2.h" #include "lv2/lv2plug.in/ns/lv2core/lv2_util.h" -#include "uris.h" #include "atom_sink.h" +#include "peaks.h" +#include "uris.h" enum { SAMPLER_CONTROL = 0, @@ -60,16 +61,15 @@ typedef struct { 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; + // 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; @@ -113,19 +113,25 @@ load_sample(LV2_Log_Logger* logger, const char* path) Sample* const sample = (Sample*)malloc(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 (!sndfile || !info->frames || (info->channels != 1)) { - lv2_log_error(logger, "Failed to open sample '%s'\n", path); + if (error) { free(sample); + free(data); + sf_close(sndfile); 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); @@ -155,7 +161,7 @@ free_sample(Sampler* self, Sample* sample) 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. + thread using the provided `respond` function. */ static LV2_Worker_Status work(LV2_Handle instance, @@ -275,6 +281,7 @@ instantiate(const LV2_Descriptor* descriptor, // 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); return (LV2_Handle)self; } @@ -308,6 +315,7 @@ run(LV2_Handle instance, { Sampler* self = (Sampler*)instance; SamplerURIs* uris = &self->uris; + PeaksURIs* peaks_uris = &self->psend.uris; sf_count_t start_frame = 0; sf_count_t pos = 0; float* output = self->output_port; @@ -378,11 +386,25 @@ run(LV2_Handle instance, } } } 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); + 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); @@ -393,6 +415,8 @@ run(LV2_Handle instance, } } + peaks_sender_send(&self->psend, &self->forge, sample_count, self->frame_offset); + // Render the sample (possibly already in progress) if (self->play) { uint32_t f = self->frame; diff --git a/plugins/eg-sampler.lv2/sampler.ttl b/plugins/eg-sampler.lv2/sampler.ttl index f4a9c43..c6103a3 100644 --- a/plugins/eg-sampler.lv2/sampler.ttl +++ b/plugins/eg-sampler.lv2/sampler.ttl @@ -16,7 +16,7 @@ <http://lv2plug.in/plugins/eg-sampler> a lv2:Plugin ; - doap:name "Example Sampler" ; + doap:name "Exampler" ; doap:license <http://opensource.org/licenses/isc> ; lv2:project <http://lv2plug.in/ns/lv2> ; lv2:requiredFeature state:loadDefaultState , diff --git a/plugins/eg-sampler.lv2/sampler_ui.c b/plugins/eg-sampler.lv2/sampler_ui.c index 23204c5..2373a4d 100644 --- a/plugins/eg-sampler.lv2/sampler_ui.c +++ b/plugins/eg-sampler.lv2/sampler_ui.c @@ -28,64 +28,136 @@ #include "lv2/lv2plug.in/ns/extensions/ui/ui.h" #include "lv2/lv2plug.in/ns/lv2core/lv2_util.h" -#include "./uris.h" +#include "peaks.h" +#include "uris.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* button; - GtkWidget* label; + 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_load_clicked(GtkWidget* widget, - void* handle) +on_file_set(GtkFileChooserButton* 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); + // Get the filename from the file chooser + char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(widget)); - /* Run the dialog, and return if it is cancelled. */ - if (gtk_dialog_run(GTK_DIALOG(dialog)) != GTK_RESPONSE_ACCEPT) { - gtk_widget_destroy(dialog); - return; - } + // 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)); - /* Get the file path from the dialog. */ - char* filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + ui->write(ui->controller, 0, lv2_atom_total_size(msg), + ui->uris.atom_eventTransfer, + msg); - /* Got what we need, destroy the dialog. */ - gtk_widget_destroy(dialog); + g_free(filename); +} -#define OBJ_BUF_SIZE 1024 - uint8_t obj_buf[OBJ_BUF_SIZE]; - lv2_atom_forge_set_buffer(&ui->forge, obj_buf, OBJ_BUF_SIZE); +static void +request_peaks(SamplerUI* ui, uint32_t n_peaks) +{ + 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)); + 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); - g_free(filename); + 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)); + + 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 @@ -104,6 +176,7 @@ instantiate(const LV2UI_Descriptor* descriptor, ui->write = write_function; ui->controller = controller; + ui->width = MIN_CANVAS_W; *widget = NULL; // Get host features @@ -122,21 +195,25 @@ instantiate(const LV2UI_Descriptor* descriptor, // 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->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->button = gtk_file_chooser_button_new("Load Sample", GTK_FILE_CHOOSER_ACTION_OPEN); + ui->canvas = gtk_drawing_area_new(); + 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, FALSE, TRUE, 0); + g_signal_connect(ui->button, "file-set", + G_CALLBACK(on_file_set), + ui); + g_signal_connect(G_OBJECT(ui->canvas), "expose_event", + G_CALLBACK(on_canvas_expose), 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_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); @@ -171,11 +248,21 @@ port_event(LV2UI_Handle handle, 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"); + 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->button), path); + peaks_receiver_clear(&ui->precv); + 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"); diff --git a/plugins/eg-sampler.lv2/uris.h b/plugins/eg-sampler.lv2/uris.h index ceeddc0..9e44cf4 100644 --- a/plugins/eg-sampler.lv2/uris.h +++ b/plugins/eg-sampler.lv2/uris.h @@ -24,9 +24,9 @@ #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" +#define EG_SAMPLER__sample EG_SAMPLER_URI "#sample" typedef struct { LV2_URID atom_Float; @@ -36,12 +36,13 @@ typedef struct { LV2_URID atom_URID; LV2_URID atom_eventTransfer; LV2_URID eg_applySample; - LV2_URID eg_sample; 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; @@ -62,17 +63,21 @@ map_sampler_uris(LV2_URID_Map* map, SamplerURIs* uris) 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 @p forge: - * [] - * a patch:Set ; - * patch:property eg:sample ; - * patch:value </home/me/foo.wav> . - */ + 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, @@ -93,12 +98,15 @@ write_set_file(LV2_Atom_Forge* forge, } /** - * Get the file path from a message like: - * [] - * a patch:Set ; - * patch:property eg:sample ; - * patch:value </home/me/foo.wav> . - */ + 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) |