aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2016-10-04 00:46:56 -0400
committerDavid Robillard <d@drobilla.net>2016-10-04 00:47:20 -0400
commit472556ff28b99db416d6f0a926c32ea28698e1a3 (patch)
treee115542762a966d0303c8f226f5984d5fb378ac9
parent2813b3f9f6a778641b3ff409a43e969cd8a8fc20 (diff)
downloadlv2-472556ff28b99db416d6f0a926c32ea28698e1a3.tar.xz
eg-sampler: Add waveform display to UI
-rw-r--r--lv2/lv2plug.in/ns/meta/meta.ttl9
-rw-r--r--plugins/eg-sampler.lv2/README.txt1
-rw-r--r--plugins/eg-sampler.lv2/peaks.h270
-rw-r--r--plugins/eg-sampler.lv2/sampler.c64
-rw-r--r--plugins/eg-sampler.lv2/sampler.ttl2
-rw-r--r--plugins/eg-sampler.lv2/sampler_ui.c169
-rw-r--r--plugins/eg-sampler.lv2/uris.h36
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)