// Copyright 2013 Robin Gareus // SPDX-License-Identifier: ISC #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/ui/ui.h" #include "lv2/urid/urid.h" #include #include #include #include #include #include #include #include #include #include #include #include // Drawing area size #define DAWIDTH (640) #define DAHEIGHT (200) /** Max continuous points on path. Many short-path segments are expensive|inefficient long paths are not supported by all surfaces (usually its a miter - not point - limit, depending on used cairo backend) */ #define MAX_CAIRO_PATH (128) /** Representation of the raw audio-data for display (min | max) values for a given 'index' position. */ typedef struct { float data_min[DAWIDTH]; float data_max[DAWIDTH]; uint32_t idx; uint32_t sub; } ScoChan; typedef struct { LV2_Atom_Forge forge; LV2_URID_Map* map; ScoLV2URIs uris; LV2UI_Write_Function write; LV2UI_Controller controller; GtkWidget* hbox; GtkWidget* vbox; GtkWidget* sep[2]; GtkWidget* darea; GtkWidget* btn_pause; GtkWidget* lbl_speed; GtkWidget* lbl_amp; GtkWidget* spb_speed; GtkWidget* spb_amp; GtkAdjustment* spb_speed_adj; GtkAdjustment* spb_amp_adj; ScoChan chn[2]; uint32_t stride; uint32_t n_channels; bool paused; float rate; bool updating; } EgScopeUI; /** Send current UI settings to backend. */ static void send_ui_state(LV2UI_Handle handle) { EgScopeUI* ui = (EgScopeUI*)handle; const float gain = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_amp)); // Use local buffer on the stack to build atom uint8_t obj_buf[1024]; lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf)); // Start a ui:State object LV2_Atom_Forge_Frame frame; LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object(&ui->forge, &frame, 0, ui->uris.ui_State); assert(msg); // msg[samples-per-pixel] = integer lv2_atom_forge_key(&ui->forge, ui->uris.ui_spp); lv2_atom_forge_int(&ui->forge, ui->stride); // msg[amplitude] = float lv2_atom_forge_key(&ui->forge, ui->uris.ui_amp); lv2_atom_forge_float(&ui->forge, gain); // Finish ui:State object lv2_atom_forge_pop(&ui->forge, &frame); // Send message to plugin port '0' ui->write(ui->controller, 0, lv2_atom_total_size(msg), ui->uris.atom_eventTransfer, msg); } /** Notify backend that UI is closed. */ static void send_ui_disable(LV2UI_Handle handle) { EgScopeUI* ui = (EgScopeUI*)handle; send_ui_state(handle); uint8_t obj_buf[64]; lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf)); LV2_Atom_Forge_Frame frame; LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object(&ui->forge, &frame, 0, ui->uris.ui_Off); assert(msg); lv2_atom_forge_pop(&ui->forge, &frame); ui->write(ui->controller, 0, lv2_atom_total_size(msg), ui->uris.atom_eventTransfer, msg); } /** Notify backend that UI is active. The plugin should send state and enable data transmission. */ static void send_ui_enable(LV2UI_Handle handle) { EgScopeUI* ui = (EgScopeUI*)handle; uint8_t obj_buf[64]; lv2_atom_forge_set_buffer(&ui->forge, obj_buf, sizeof(obj_buf)); LV2_Atom_Forge_Frame frame; LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object(&ui->forge, &frame, 0, ui->uris.ui_On); assert(msg); lv2_atom_forge_pop(&ui->forge, &frame); ui->write(ui->controller, 0, lv2_atom_total_size(msg), ui->uris.atom_eventTransfer, msg); } /** Gtk widget callback. */ static gboolean on_cfg_changed(GtkWidget* widget, gpointer data) { const EgScopeUI* ui = (const EgScopeUI*)data; if (!ui->updating) { // Only send UI state if the change is from user interaction send_ui_state(data); } return TRUE; } /** Gdk drawing area draw callback. Called in Gtk's main thread and uses Cairo to draw the data. */ static gboolean on_expose_event(GtkWidget* widget, GdkEventExpose* ev, gpointer data) { EgScopeUI* ui = (EgScopeUI*)data; const float gain = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_amp)); // Get cairo type for the gtk window cairo_t* cr = gdk_cairo_create(ui->darea->window); // Limit cairo-drawing to exposed area cairo_rectangle(cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height); cairo_clip(cr); // Clear background cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0); cairo_rectangle(cr, 0, 0, DAWIDTH, DAHEIGHT * ui->n_channels); cairo_fill(cr); cairo_set_line_width(cr, 1.0); const uint32_t start = ev->area.x; const uint32_t end = ev->area.x + ev->area.width; assert(start < DAWIDTH); assert(end <= DAWIDTH); assert(start < end); for (uint32_t c = 0; c < ui->n_channels; ++c) { ScoChan* chn = &ui->chn[c]; /* Drawing area Y-position of given sample-value. * Note: cairo-pixel at 0 spans -0.5 .. +0.5, hence (DAHEIGHT / 2.0 -0.5) * also the cairo Y-axis points upwards (hence 'minus value') * * == ( DAHEIGHT * (CHN) // channel offset * + (DAHEIGHT / 2) - 0.5 // vertical center -- '0' * - (DAHEIGHT / 2) * (VAL) * (GAIN) * ) */ const float chn_y_offset = DAHEIGHT * c + DAHEIGHT * 0.5f - 0.5f; const float chn_y_scale = DAHEIGHT * 0.5f * gain; #define CYPOS(VAL) (chn_y_offset - (VAL) * chn_y_scale) cairo_save(cr); /* Restrict drawing to current channel area, don't bleed drawing into neighboring channels. */ cairo_rectangle(cr, 0, DAHEIGHT * c, DAWIDTH, DAHEIGHT); cairo_clip(cr); // Set color of wave-form cairo_set_source_rgba(cr, 0.0, 1.0, 0.0, 1.0); /* This is a somewhat 'smart' mechanism to plot audio data using alternating up/down line-directions. It works well for both cases: 1 pixel <= 1 sample and 1 pixel represents more than 1 sample, but is not ideal for either. */ if (start == chn->idx) { cairo_move_to(cr, start - 0.5, CYPOS(0)); } else { cairo_move_to(cr, start - 0.5, CYPOS(chn->data_max[start])); } uint32_t pathlength = 0; for (uint32_t i = start; i < end; ++i) { if (i == chn->idx) { continue; } if (i % 2) { cairo_line_to(cr, i - .5, CYPOS(chn->data_min[i])); cairo_line_to(cr, i - .5, CYPOS(chn->data_max[i])); ++pathlength; } else { cairo_line_to(cr, i - .5, CYPOS(chn->data_max[i])); cairo_line_to(cr, i - .5, CYPOS(chn->data_min[i])); ++pathlength; } /** Limit the max cairo path length. This is an optimization trade off: too short path: high load CPU/GPU load. too-long path: bad anti-aliasing, or possibly lost points */ if (pathlength > MAX_CAIRO_PATH) { pathlength = 0; cairo_stroke(cr); if (i % 2) { cairo_move_to(cr, i - .5, CYPOS(chn->data_max[i])); } else { cairo_move_to(cr, i - .5, CYPOS(chn->data_min[i])); } } } if (pathlength > 0) { cairo_stroke(cr); } // Draw current position vertical line if display is slow if (ui->stride >= ui->rate / 4800.0f || ui->paused) { cairo_set_source_rgba(cr, .9, .2, .2, .6); cairo_move_to(cr, chn->idx - .5, DAHEIGHT * c); cairo_line_to(cr, chn->idx - .5, DAHEIGHT * (c + 1)); cairo_stroke(cr); } // Undo the 'clipping' restriction cairo_restore(cr); // Channel separator if (c > 0) { cairo_set_source_rgba(cr, .5, .5, .5, 1.0); cairo_move_to(cr, 0, DAHEIGHT * c - .5); cairo_line_to(cr, DAWIDTH, DAHEIGHT * c - .5); cairo_stroke(cr); } // Zero scale line cairo_set_source_rgba(cr, .3, .3, .7, .5); cairo_move_to(cr, 0, DAHEIGHT * (c + .5) - .5); cairo_line_to(cr, DAWIDTH, DAHEIGHT * (c + .5) - .5); cairo_stroke(cr); } cairo_destroy(cr); return TRUE; } /** Parse raw audio data and prepare for later drawing. Note this is a toy example, which is really a waveform display, not an oscilloscope. A serious scope would not display samples as is. Signals above ~ 1/10 of the sampling-rate will not yield a useful visual display and result in a rather unintuitive representation of the actual waveform. Ideally the audio-data would be buffered and upsampled here and after that written in a display buffer for later use. For more information, see https://wiki.xiph.org/Videos/Digital_Show_and_Tell http://lac.linuxaudio.org/2013/papers/36.pdf and https://github.com/x42/sisco.lv2 */ static int process_channel(const EgScopeUI* ui, ScoChan* chn, const size_t n_elem, float const* data, uint32_t* idx_start, uint32_t* idx_end) { int overflow = 0; *idx_start = chn->idx; for (size_t i = 0; i < n_elem; ++i) { if (data[i] < chn->data_min[chn->idx]) { chn->data_min[chn->idx] = data[i]; } if (data[i] > chn->data_max[chn->idx]) { chn->data_max[chn->idx] = data[i]; } if (++chn->sub >= ui->stride) { chn->sub = 0; chn->idx = (chn->idx + 1) % DAWIDTH; if (chn->idx == 0) { ++overflow; } chn->data_min[chn->idx] = 1.0f; chn->data_max[chn->idx] = -1.0f; } } *idx_end = chn->idx; return overflow; } /** Called via port_event() which is called by the host, typically at a rate of around 25 FPS. */ static void update_scope(EgScopeUI* ui, const int32_t channel, const size_t n_elem, float const* data) { // Never trust input data which could lead to application failure. if (channel < 0 || (uint32_t)channel > ui->n_channels) { return; } // Update state in sync with 1st channel if (channel == 0) { ui->stride = gtk_spin_button_get_value(GTK_SPIN_BUTTON(ui->spb_speed)); const bool paused = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(ui->btn_pause)); if (paused != ui->paused) { ui->paused = paused; gtk_widget_queue_draw(ui->darea); } } if (ui->paused) { return; } uint32_t idx_start = 0; // Display pixel start uint32_t idx_end = 0; // Display pixel end int overflow = 0; // Received more audio-data than display-pixel // Process this channel's audio-data for display ScoChan* chn = &ui->chn[channel]; overflow = process_channel(ui, chn, n_elem, data, &idx_start, &idx_end); // Signal gtk's main thread to redraw the widget after the last channel if ((uint32_t)channel + 1 == ui->n_channels) { if (overflow > 1) { // Redraw complete widget gtk_widget_queue_draw(ui->darea); } else if (idx_end > idx_start) { // Redraw area between start -> end pixel gtk_widget_queue_draw_area(ui->darea, idx_start - 2, 0, 3 + idx_end - idx_start, DAHEIGHT * ui->n_channels); } else if (idx_end < idx_start) { // Wrap-around: redraw area between 0->start AND end->right-end gtk_widget_queue_draw_area(ui->darea, idx_start - 2, 0, 3 + DAWIDTH - idx_start, DAHEIGHT * ui->n_channels); gtk_widget_queue_draw_area( ui->darea, 0, 0, idx_end + 1, DAHEIGHT * ui->n_channels); } } } 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) { EgScopeUI* ui = (EgScopeUI*)calloc(1, sizeof(EgScopeUI)); if (!ui) { fprintf(stderr, "EgScope.lv2 UI: out of memory\n"); return NULL; } ui->map = NULL; *widget = NULL; if (!strcmp(plugin_uri, SCO_URI "#Mono")) { ui->n_channels = 1; } else if (!strcmp(plugin_uri, SCO_URI "#Stereo")) { ui->n_channels = 2; } else { free(ui); return NULL; } for (int i = 0; features[i]; ++i) { if (!strcmp(features[i]->URI, LV2_URID_URI "#map")) { ui->map = (LV2_URID_Map*)features[i]->data; } } if (!ui->map) { fprintf(stderr, "EgScope.lv2 UI: Host does not support urid:map\n"); free(ui); return NULL; } // Initialize private data structure ui->write = write_function; ui->controller = controller; ui->vbox = NULL; ui->hbox = NULL; ui->darea = NULL; ui->stride = 25; ui->paused = false; ui->rate = 48000; ui->chn[0].idx = 0; ui->chn[0].sub = 0; ui->chn[1].idx = 0; ui->chn[1].sub = 0; memset(ui->chn[0].data_min, 0, sizeof(float) * DAWIDTH); memset(ui->chn[0].data_max, 0, sizeof(float) * DAWIDTH); memset(ui->chn[1].data_min, 0, sizeof(float) * DAWIDTH); memset(ui->chn[1].data_max, 0, sizeof(float) * DAWIDTH); map_sco_uris(ui->map, &ui->uris); lv2_atom_forge_init(&ui->forge, ui->map); // Setup UI ui->hbox = gtk_hbox_new(FALSE, 0); ui->vbox = gtk_vbox_new(FALSE, 0); ui->darea = gtk_drawing_area_new(); gtk_widget_set_size_request(ui->darea, DAWIDTH, DAHEIGHT * ui->n_channels); ui->lbl_speed = gtk_label_new("Samples/Pixel"); ui->lbl_amp = gtk_label_new("Amplitude"); ui->sep[0] = gtk_hseparator_new(); ui->sep[1] = gtk_label_new(""); ui->btn_pause = gtk_toggle_button_new_with_label("Pause"); ui->spb_speed_adj = (GtkAdjustment*)gtk_adjustment_new(25.0, 1.0, 1000.0, 1.0, 5.0, 0.0); ui->spb_speed = gtk_spin_button_new(ui->spb_speed_adj, 1.0, 0); ui->spb_amp_adj = (GtkAdjustment*)gtk_adjustment_new(1.0, 0.1, 6.0, 0.1, 1.0, 0.0); ui->spb_amp = gtk_spin_button_new(ui->spb_amp_adj, 0.1, 1); gtk_box_pack_start(GTK_BOX(ui->hbox), ui->darea, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(ui->hbox), ui->vbox, FALSE, FALSE, 4); gtk_box_pack_start(GTK_BOX(ui->vbox), ui->lbl_speed, FALSE, FALSE, 2); gtk_box_pack_start(GTK_BOX(ui->vbox), ui->spb_speed, FALSE, FALSE, 2); gtk_box_pack_start(GTK_BOX(ui->vbox), ui->sep[0], FALSE, FALSE, 8); gtk_box_pack_start(GTK_BOX(ui->vbox), ui->lbl_amp, FALSE, FALSE, 2); gtk_box_pack_start(GTK_BOX(ui->vbox), ui->spb_amp, FALSE, FALSE, 2); gtk_box_pack_start(GTK_BOX(ui->vbox), ui->sep[1], TRUE, FALSE, 8); gtk_box_pack_start(GTK_BOX(ui->vbox), ui->btn_pause, FALSE, FALSE, 2); g_signal_connect( G_OBJECT(ui->darea), "expose_event", G_CALLBACK(on_expose_event), ui); g_signal_connect( G_OBJECT(ui->spb_amp), "value-changed", G_CALLBACK(on_cfg_changed), ui); g_signal_connect( G_OBJECT(ui->spb_speed), "value-changed", G_CALLBACK(on_cfg_changed), ui); *widget = ui->hbox; /* Send UIOn message to plugin, which will request state and enable message transmission. */ send_ui_enable(ui); return ui; } static void cleanup(LV2UI_Handle handle) { EgScopeUI* ui = (EgScopeUI*)handle; /* Send UIOff message to plugin, which will save state and disable message * transmission. */ send_ui_disable(ui); gtk_widget_destroy(ui->darea); free(ui); } static int recv_raw_audio(EgScopeUI* ui, const LV2_Atom_Object* obj) { const LV2_Atom* chan_val = NULL; const LV2_Atom* data_val = NULL; const int n_props = lv2_atom_object_get( obj, ui->uris.channelID, &chan_val, ui->uris.audioData, &data_val, NULL); if (n_props != 2 || chan_val->type != ui->uris.atom_Int || data_val->type != ui->uris.atom_Vector) { // Object does not have the required properties with correct types fprintf(stderr, "eg-scope.lv2 UI error: Corrupt audio message\n"); return 1; } // Get the values we need from the body of the property value atoms const int32_t chn = ((const LV2_Atom_Int*)chan_val)->body; const LV2_Atom_Vector* vec = (const LV2_Atom_Vector*)data_val; if (vec->body.child_type != ui->uris.atom_Float) { return 1; // Vector has incorrect element type } // Number of elements = (total size - header size) / element size const size_t n_elem = ((data_val->size - sizeof(LV2_Atom_Vector_Body)) / sizeof(float)); // Float elements immediately follow the vector body header const float* data = (const float*)(&vec->body + 1); // Update display update_scope(ui, chn, n_elem, data); return 0; } static int recv_ui_state(EgScopeUI* ui, const LV2_Atom_Object* obj) { const LV2_Atom* spp_val = NULL; const LV2_Atom* amp_val = NULL; const LV2_Atom* rate_val = NULL; const int n_props = lv2_atom_object_get(obj, ui->uris.ui_spp, &spp_val, ui->uris.ui_amp, &_val, ui->uris.param_sampleRate, &rate_val, NULL); if (n_props != 3 || spp_val->type != ui->uris.atom_Int || amp_val->type != ui->uris.atom_Float || rate_val->type != ui->uris.atom_Float) { // Object does not have the required properties with correct types fprintf(stderr, "eg-scope.lv2 UI error: Corrupt state message\n"); return 1; } // Get the values we need from the body of the property value atoms const int32_t spp = ((const LV2_Atom_Int*)spp_val)->body; const float amp = ((const LV2_Atom_Float*)amp_val)->body; const float rate = ((const LV2_Atom_Float*)rate_val)->body; // Disable transmission and update UI ui->updating = true; gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->spb_speed), spp); gtk_spin_button_set_value(GTK_SPIN_BUTTON(ui->spb_amp), amp); ui->updating = false; ui->rate = rate; return 0; } /** Receive data from the DSP-backend. This is called by the host, typically at a rate of around 25 FPS. Ideally this happens regularly and with relatively low latency, but there are no hard guarantees about message delivery. */ static void port_event(LV2UI_Handle handle, uint32_t port_index, uint32_t buffer_size, uint32_t format, const void* buffer) { EgScopeUI* ui = (EgScopeUI*)handle; const LV2_Atom* atom = (const LV2_Atom*)buffer; /* Check type of data received * - format == 0: Control port event (float) * - format > 0: Message (atom) */ if (format == ui->uris.atom_eventTransfer && 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.RawAudio) { recv_raw_audio(ui, obj); } else if (obj->body.otype == ui->uris.ui_State) { recv_ui_state(ui, obj); } } } static const LV2UI_Descriptor descriptor = {SCO_URI "#ui", instantiate, cleanup, port_event, NULL}; LV2_SYMBOL_EXPORT const LV2UI_Descriptor* lv2ui_descriptor(uint32_t index) { return index == 0 ? &descriptor : NULL; }