From 5fd1ec15222b1d42258cd8af3d62281ea966b81a Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Sun, 10 Mar 2019 20:45:31 +0000
Subject: [PATCH] Start adding humanization UI controls

---
 Makefile.am           |   3 +-
 aux/verify-voice.c    |   2 +-
 engine/voice.c        |   5 +-
 engine/voice.h        |   2 +-
 plugin/ui.c           |  66 +++++++-
 plugin/widgets/knob.c | 371 ++++++++++++++++++++++++++++++++++++++++++
 plugin/widgets/knob.h |  72 ++++++++
 7 files changed, 514 insertions(+), 7 deletions(-)
 create mode 100644 plugin/widgets/knob.c
 create mode 100644 plugin/widgets/knob.h

diff --git a/Makefile.am b/Makefile.am
index 20018fe..d36a5f3 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -35,7 +35,8 @@ hydrumkit_la_LIBADD = libengine.la
 hydrumkit_ui_la_SOURCES = \
 	plugin/ui.c \
 	plugin/uris.h \
-	plugin/lv2_util.h
+	plugin/lv2_util.h \
+	plugin/widgets/knob.c plugin/widgets/knob.h
 hydrumkit_ui_la_CPPFLAGS = $(GTK_CFLAGS)
 hydrumkit_ui_la_LDFLAGS = -module -avoid-version -shared
 hydrumkit_ui_la_LIBADD = libengine.la $(GTK_LIBS)
diff --git a/aux/verify-voice.c b/aux/verify-voice.c
index c71f823..6c12cb2 100644
--- a/aux/verify-voice.c
+++ b/aux/verify-voice.c
@@ -21,7 +21,7 @@ void write_sample(const char *filename, float *data_l, float *data_r, int num_fr
 
   info.samplerate = SR;
   info.channels = 2;
-  info.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16;
+  info.format = SF_FORMAT_WAV | SF_FORMAT_FLOAT;
   
   sndfile = sf_open(filename, SFM_WRITE, &info);
   sf_writef_float(sndfile, buf, num_frames);
diff --git a/engine/voice.c b/engine/voice.c
index a0a7f9d..f8369c8 100644
--- a/engine/voice.c
+++ b/engine/voice.c
@@ -44,6 +44,7 @@ int voice_noteon(struct voice *voice, struct instrument *ins, int note, int velo
     voice->in_preamble = 1;
     voice->preamble_countdown = preamble;
   } else {
+    voice->in_preamble = 0;
     voice->playing = 1;
     voice->ticks = 0;
   }    
@@ -52,7 +53,7 @@ int voice_noteon(struct voice *voice, struct instrument *ins, int note, int velo
 }
 
 int voice_noteoff_if_note(struct voice *voice, int note) {
-  if ((voice->playing || voice->preamble_countdown) && voice->note == note) {
+  if ((voice->playing || voice->in_preamble) && voice->note == note) {
     adsr_release(&voice->adsr);
     return 1;
   }
@@ -60,7 +61,7 @@ int voice_noteoff_if_note(struct voice *voice, int note) {
 }
 
 int voice_noteoff_if_mute_group(struct voice *voice, int mute_group) {
-  if ((voice->playing || voice->preamble_countdown) && voice->mute_group == mute_group) {
+  if ((voice->playing || voice->in_preamble) && voice->mute_group == mute_group) {
     adsr_release(&voice->adsr);
     return 1;
   }
diff --git a/engine/voice.h b/engine/voice.h
index aedd0ac..0a49b32 100644
--- a/engine/voice.h
+++ b/engine/voice.h
@@ -26,7 +26,7 @@ int voice_noteoff_if_mute_group(struct voice *, int);
 void voice_process(struct voice *, float *, float *, int);
 
 static inline int voice_is_playing(struct voice *voice) {
-  return voice->playing | voice->in_preamble;
+  return voice->playing || voice->in_preamble;
 }
 
 #endif
diff --git a/plugin/ui.c b/plugin/ui.c
index d1f5576..04c7615 100644
--- a/plugin/ui.c
+++ b/plugin/ui.c
@@ -22,6 +22,7 @@
 #include <glib.h>
 #include <gobject/gclosure.h>
 #include <gtk/gtk.h>
+#include "plugin/widgets/knob.h"
 
 #include <stdint.h>
 #include <stdlib.h>
@@ -30,6 +31,8 @@
 
 #define SAMPLER_UI_URI "http://lv2.incal.net/plugins/hydrumkit#ui"
 
+#define KNOB_SIZE 32
+
 struct plugin_ui {
   LV2_Atom_Forge forge;
   LV2_URID_Map *map;
@@ -44,8 +47,20 @@ struct plugin_ui {
   GtkWidget *load_dialog;
   GtkWidget *button_box;
   GtkWidget *box;
+  GtkWidget *humanizer_box;
+  GtkWidget *humanizer_button;
+  GtkWidget *humanizer_latency_max_knob;
+  GtkWidget *humanizer_latency_stddev_knob;
+  GtkWidget *humanizer_latency_laid_back_knob;
+  GtkWidget *humanizer_velocity_knob;
   GtkWidget *window; /* For optional show interface. */
 
+  int humanizer_enabled;
+  float humanizer_latency_max_value;
+  float humanizer_latency_stddev_value;
+  float humanizer_latency_laid_back_value;
+  float humanizer_velocity_value;
+
   char *filename;
 
   uint8_t forge_buf[1024];
@@ -95,6 +110,22 @@ static void on_load_dialog_response(GtkDialog *widget, gint response, void *hand
   };
 }
 
+static void on_humanizer_button_click(GtkButton *widget, void *handle) {
+  struct plugin_ui *ui = (struct plugin_ui *)handle;
+
+  ui->humanizer_enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
+  printf("humanizer %s\n", ui->humanizer_enabled ? "enabled" : "disabled");
+}
+
+static void on_humanizer_knob_change(KnobWidget *widget, void *handle) {
+  struct plugin_ui *ui = (struct plugin_ui *)handle;
+  printf("humanizer value change: latency=%g/%g/%g velocity=%g\n",
+         ui->humanizer_latency_max_value,
+         ui->humanizer_latency_stddev_value,
+         ui->humanizer_latency_laid_back_value,
+         ui->humanizer_velocity_value);
+}
+
 static int path_exists(const char *path) {
   struct stat stbuf;
   return (0 == stat(path, &stbuf)) ? 1 : 0;
@@ -138,9 +169,14 @@ static LV2UI_Handle instantiate(const LV2UI_Descriptor *descriptor,
 
   // Construct Gtk UI
   ui->box = gtk_grid_new();
+  gtk_orientable_set_orientation(GTK_ORIENTABLE(ui->box), GTK_ORIENTATION_VERTICAL);
   ui->label = gtk_label_new("No drumkit loaded.");
   ui->button_box = gtk_grid_new();
+  ui->humanizer_box = gtk_grid_new();
 
+  // Create the file chooser (we use a custom GtkButton instead of the
+  // default GtkFileChooserButton because the latter takes way too
+  // much UI real estate).
   filter = gtk_file_filter_new();
   gtk_file_filter_add_pattern(filter, "drumkit.xml");
 
@@ -170,14 +206,33 @@ static LV2UI_Handle instantiate(const LV2UI_Descriptor *descriptor,
     }
     free(homebuf);
   }
-  
+
+  // Create the humanizer controls.
+  ui->humanizer_button = gtk_check_button_new_with_label("Humanize");
+  ui->humanizer_latency_max_knob = knob_widget_new(&ui->humanizer_latency_max_value, KNOB_SIZE, 0);
+  ui->humanizer_latency_stddev_knob = knob_widget_new(&ui->humanizer_latency_stddev_value, KNOB_SIZE, 0);
+  ui->humanizer_latency_laid_back_knob = knob_widget_new(&ui->humanizer_latency_laid_back_value, KNOB_SIZE, 0);
+  ui->humanizer_velocity_knob = knob_widget_new(&ui->humanizer_velocity_value, KNOB_SIZE, 0);
+  g_signal_connect(ui->humanizer_button, "clicked", G_CALLBACK(on_humanizer_button_click), ui);
+  g_signal_connect(ui->humanizer_latency_max_knob, "value-changed", G_CALLBACK(on_humanizer_knob_change), ui);
+  g_signal_connect(ui->humanizer_latency_stddev_knob, "value-changed", G_CALLBACK(on_humanizer_knob_change), ui);
+  g_signal_connect(ui->humanizer_latency_laid_back_knob, "value-changed", G_CALLBACK(on_humanizer_knob_change), ui);
+  g_signal_connect(ui->humanizer_velocity_knob, "value-changed", G_CALLBACK(on_humanizer_knob_change), ui);
+
+  // Place all elements inside their containers.
   gtk_container_set_border_width(GTK_CONTAINER(ui->box), 4);
-  gtk_container_add(GTK_CONTAINER(ui->box), ui->button_box);
   gtk_container_add(GTK_CONTAINER(ui->button_box), ui->label);
   gtk_widget_set_hexpand(ui->label, TRUE);
   gtk_widget_set_halign(ui->label, GTK_ALIGN_CENTER);
   gtk_container_add(GTK_CONTAINER(ui->button_box), ui->load_button);
   gtk_widget_set_hexpand(ui->load_button, FALSE);
+  gtk_container_add(GTK_CONTAINER(ui->box), ui->button_box);
+  gtk_container_add(GTK_CONTAINER(ui->humanizer_box), ui->humanizer_button);
+  gtk_container_add(GTK_CONTAINER(ui->humanizer_box), ui->humanizer_latency_max_knob);
+  gtk_container_add(GTK_CONTAINER(ui->humanizer_box), ui->humanizer_latency_stddev_knob);
+  gtk_container_add(GTK_CONTAINER(ui->humanizer_box), ui->humanizer_latency_laid_back_knob);
+  gtk_container_add(GTK_CONTAINER(ui->humanizer_box), ui->humanizer_velocity_knob);
+  gtk_container_add(GTK_CONTAINER(ui->box), ui->humanizer_box);
 
   g_signal_connect(ui->load_dialog, "response", G_CALLBACK(on_load_dialog_response), ui);
 
@@ -202,6 +257,13 @@ static void cleanup(LV2UI_Handle handle) {
   gtk_widget_destroy(ui->load_dialog);
   gtk_widget_destroy(ui->load_button);
   gtk_widget_destroy(ui->button_box);
+  gtk_widget_destroy(ui->humanizer_button);
+  gtk_widget_destroy(ui->humanizer_latency_max_knob);
+  gtk_widget_destroy(ui->humanizer_latency_stddev_knob);
+  gtk_widget_destroy(ui->humanizer_latency_laid_back_knob);
+  gtk_widget_destroy(ui->humanizer_velocity_knob);
+  gtk_widget_destroy(ui->humanizer_box);
+
   gtk_widget_destroy(ui->box);
   free(ui);
 }
diff --git a/plugin/widgets/knob.c b/plugin/widgets/knob.c
new file mode 100644
index 0000000..a6b9657
--- /dev/null
+++ b/plugin/widgets/knob.c
@@ -0,0 +1,371 @@
+/*
+ * gui/widgets/knob.c - knob
+ *
+ * Copyright (C) 2018 Alexandros Theodotou
+ * Copyright (C) 2010 Paul Davis
+ *
+ * This file is part of Zrythm
+ *
+ * Zrythm is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Zrythm is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Zrythm.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/** \file
+ */
+#include <stdint.h>
+#include <stdlib.h>
+#include <math.h>
+
+#include "plugin/widgets/knob.h"
+
+#include <gtk/gtk.h>
+
+G_DEFINE_TYPE (KnobWidget, knob_widget, GTK_TYPE_DRAWING_AREA)
+
+enum {
+  VALUE_CHANGED,
+  LAST_SIGNAL
+};
+
+static guint knob_signals[LAST_SIGNAL] = { 0 };
+
+/**
+ * Draws the knob.
+ */
+static int
+draw_cb (GtkWidget * widget, cairo_t * cr, void* data)
+{
+  guint width, height;
+  GdkRGBA color;
+  GtkStyleContext *context;
+  KnobWidget * self = (KnobWidget *) data;
+
+  context = gtk_widget_get_style_context (widget);
+
+  width = gtk_widget_get_allocated_width (widget);
+  height = gtk_widget_get_allocated_height (widget);
+
+  gtk_render_background (context, cr, 0, 0, width, height);
+  cairo_pattern_t* shade_pattern;
+
+  const float scale = width;
+  const float pointer_thickness = 3.0 * (scale/80);  //(if the knob is 80 pixels wide, we want a 3-pix line on it)
+
+  const float start_angle = ((180 - 65) * G_PI) / 180;
+  const float end_angle = ((360 + 65) * G_PI) / 180;
+
+  const float value_angle = start_angle + (*self->value * (end_angle - start_angle));
+  const float zero_angle = start_angle + (self->zero * (end_angle - start_angle));
+
+  float value_x = cos (value_angle);
+  float value_y = sin (value_angle);
+
+  float xc =  0.5 + width/ 2.0;
+  float yc = 0.5 + height/ 2.0;
+
+  cairo_translate (cr, xc, yc);  //after this, everything is based on the center of the knob
+
+  //get the knob color from the theme
+
+  float center_radius = 0.48*scale;
+  float border_width = 0.8;
+
+
+  if (self->arc)
+    {
+      center_radius = scale*0.33;
+
+      float inner_progress_radius = scale*0.38;
+      float outer_progress_radius = scale*0.48;
+      float progress_width = (outer_progress_radius-inner_progress_radius);
+      float progress_radius = inner_progress_radius + progress_width/2.0;
+
+      /* dark surrounding arc background */
+      cairo_set_source_rgb (cr, 0.3, 0.3, 0.3 );
+      cairo_set_line_width (cr, progress_width);
+      cairo_arc (cr, 0, 0, progress_radius, start_angle, end_angle);
+      cairo_stroke (cr);
+
+      //look up the surrounding arc colors from the config
+      // TODO
+
+      //vary the arc color over the travel of the knob
+      float intensity = fabsf (*self->value - self->zero) / MAX(self->zero, (1.f - self->zero));
+      const float intensity_inv = 1.0 - intensity;
+      float r = intensity_inv * self->end_color.red   +
+                intensity * self->start_color.red;
+      float g = intensity_inv * self->end_color.green +
+                intensity * self->start_color.green;
+      float b = intensity_inv * self->end_color.blue  +
+                intensity * self->start_color.blue;
+
+      //draw the arc
+      cairo_set_source_rgb (cr, r,g,b);
+      cairo_set_line_width (cr, progress_width);
+      if (zero_angle > value_angle)
+        {
+          cairo_arc (cr, 0, 0, progress_radius, value_angle, zero_angle);
+        }
+      else
+        {
+          cairo_arc (cr, 0, 0, progress_radius, zero_angle, value_angle);
+        }
+      cairo_stroke (cr);
+
+      //shade the arc
+      if (!self->flat)
+        {
+          shade_pattern = cairo_pattern_create_linear (0.0, -yc, 0.0,  yc);  //note we have to offset the pattern from our centerpoint
+          cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1,1,1, 0.15);
+          cairo_pattern_add_color_stop_rgba (shade_pattern, 0.5, 1,1,1, 0.0);
+          cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 1,1,1, 0.0);
+          cairo_set_source (cr, shade_pattern);
+          cairo_arc (cr, 0, 0, outer_progress_radius-1, 0, 2.0*G_PI);
+          cairo_fill (cr);
+          cairo_pattern_destroy (shade_pattern);
+        }
+
+#if 0 //black border
+      const float start_angle_x = cos (start_angle);
+      const float start_angle_y = sin (start_angle);
+      const float end_angle_x = cos (end_angle);
+      const float end_angle_y = sin (end_angle);
+
+      cairo_set_source_rgb (cr, 0, 0, 0 );
+      cairo_set_line_width (cr, border_width);
+      cairo_move_to (cr, (outer_progress_radius * start_angle_x), (outer_progress_radius * start_angle_y));
+      cairo_line_to (cr, (inner_progress_radius * start_angle_x), (inner_progress_radius * start_angle_y));
+      cairo_stroke (cr);
+      cairo_move_to (cr, (outer_progress_radius * end_angle_x), (outer_progress_radius * end_angle_y));
+      cairo_line_to (cr, (inner_progress_radius * end_angle_x), (inner_progress_radius * end_angle_y));
+      cairo_stroke (cr);
+      cairo_arc (cr, 0, 0, outer_progress_radius, start_angle, end_angle);
+      cairo_stroke (cr);
+#endif
+    }
+
+  if (!self->flat)
+    {
+      //knob shadow
+      cairo_save(cr);
+      cairo_translate(cr, pointer_thickness+1, pointer_thickness+1 );
+      cairo_set_source_rgba (cr, 0, 0, 0, 0.1 );
+      cairo_arc (cr, 0, 0, center_radius-1, 0, 2.0*G_PI);
+      cairo_fill (cr);
+      cairo_restore(cr);
+
+      //inner circle
+#define KNOB_COLOR 0, 90, 0, 0
+      cairo_set_source_rgba(cr, KNOB_COLOR); /* knob color */
+      cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
+      cairo_fill (cr);
+
+      //gradient
+      if (self->bevel)
+        {
+          //knob gradient
+          shade_pattern = cairo_pattern_create_linear (0.0, -yc, 0.0,  yc);  //note we have to offset the gradient from our centerpoint
+          cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1,1,1, 0.2);
+          cairo_pattern_add_color_stop_rgba (shade_pattern, 0.2, 1,1,1, 0.2);
+          cairo_pattern_add_color_stop_rgba (shade_pattern, 0.8, 0,0,0, 0.2);
+          cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 0,0,0, 0.2);
+          cairo_set_source (cr, shade_pattern);
+          cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
+          cairo_fill (cr);
+          cairo_pattern_destroy (shade_pattern);
+
+          //flat top over beveled edge
+          cairo_set_source_rgba (cr, 90, 0, 0, 0.5 );
+          cairo_arc (cr, 0, 0, center_radius-pointer_thickness, 0, 2.0*G_PI);
+          cairo_fill (cr);
+        }
+      else
+        {
+          //radial gradient
+          shade_pattern = cairo_pattern_create_radial ( -center_radius, -center_radius, 1, -center_radius, -center_radius, center_radius*2.5  );  //note we have to offset the gradient from our centerpoint
+          cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1,1,1, 0.2);
+          cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 0,0,0, 0.3);
+          cairo_set_source (cr, shade_pattern);
+          cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
+          cairo_fill (cr);
+          cairo_pattern_destroy (shade_pattern);
+        }
+    }
+  else
+    {
+        /* color inner circle */
+        cairo_set_source_rgba(cr, 70, 70, 70, 0.2);
+        cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
+        cairo_fill (cr);
+    }
+
+
+  //black knob border
+  cairo_set_line_width (cr, border_width);
+  cairo_set_source_rgba (cr, 0,0,0, 1 );
+  cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
+  cairo_stroke (cr);
+
+  //line shadow
+  if (!self->flat) {
+          cairo_save(cr);
+          cairo_translate(cr, 1, 1 );
+          cairo_set_source_rgba (cr, 0,0,0,0.3 );
+          cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
+          cairo_set_line_width (cr, pointer_thickness);
+          cairo_move_to (cr, (center_radius * value_x), (center_radius * value_y));
+          cairo_line_to (cr, ((center_radius*0.4) * value_x), ((center_radius*0.4) * value_y));
+          cairo_stroke (cr);
+          cairo_restore(cr);
+  }
+
+  //line
+  cairo_set_source_rgba (cr, 1,1,1, 1 );
+  cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
+  cairo_set_line_width (cr, pointer_thickness);
+  cairo_move_to (cr, (center_radius * value_x), (center_radius * value_y));
+  cairo_line_to (cr, ((center_radius*0.4) * value_x), ((center_radius*0.4) * value_y));
+  cairo_stroke (cr);
+
+  //highlight if grabbed or if mouse is hovering over me
+  if (self->hover)
+    {
+      cairo_set_source_rgba (cr, 1,1,1, 0.12 );
+      cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
+      cairo_fill (cr);
+    }
+
+  cairo_identity_matrix(cr);
+}
+
+static void
+on_crossing (GtkWidget * widget, GdkEvent *event, void * data)
+{
+  KnobWidget * self = (KnobWidget *) data;
+   switch (gdk_event_get_event_type (event))
+    {
+    case GDK_ENTER_NOTIFY:
+      self->hover = 1;
+      break;
+
+    case GDK_LEAVE_NOTIFY:
+      if (!gtk_gesture_drag_get_offset (self->drag,
+                                       NULL,
+                                       NULL))
+        self->hover = 0;
+      break;
+    }
+  gtk_widget_queue_draw(widget);
+}
+
+static double clamp
+(double x, double upper, double lower)
+{
+    return MIN(upper, MAX(x, lower));
+}
+
+static void
+drag_update (GtkGestureDrag * gesture,
+               gdouble         offset_x,
+               gdouble         offset_y,
+               gpointer        user_data)
+{
+  KnobWidget * self = (KnobWidget *) user_data;
+  offset_y = - offset_y;
+  int use_y = abs(offset_y - self->last_y) > abs(offset_x - self->last_x);
+  *self->value = clamp (*self->value + 0.004 * (use_y ? offset_y - self->last_y : offset_x - self->last_x),
+               1.0f, 0.0f);
+  self->last_x = offset_x;
+  self->last_y = offset_y;
+  gtk_widget_queue_draw ((GtkWidget *)user_data);
+}
+
+static void
+drag_end (GtkGestureDrag *gesture,
+               gdouble         offset_x,
+               gdouble         offset_y,
+               gpointer        user_data)
+{
+  KnobWidget * self = (KnobWidget *) user_data;
+  self->last_x = 0;
+  self->last_y = 0;
+
+  g_signal_emit(self, knob_signals[VALUE_CHANGED], 0);
+}
+
+
+/**
+ * Creates a knob widget with the given options and binds it to the given value.
+ */
+KnobWidget *
+knob_widget_new (float      * value,
+                    int         size,
+                    float       zero)
+{
+  KnobWidget * self = g_object_new (KNOB_WIDGET_TYPE, NULL);
+  self->value = value;
+  self->size = size; /* default 30 */
+  self->hover = 0;
+  self->zero = zero; /* default 0.05f */
+  self->value = value;
+  self->arc = 1;
+  self->bevel = 1;
+  self->flat = 1;
+  self->start_color.red = 0.8;
+  self->start_color.green = 0.8;
+  self->start_color.blue = 0.8;
+  self->end_color.red = 0.7;
+  self->end_color.red = 0.7;
+  self->end_color.red = 0.7;
+  self->last_x = 0;
+  self->last_y = 0;
+
+  /* set size */
+  gtk_widget_set_size_request (GTK_WIDGET (self), size, size);
+
+  /* connect signals */
+  g_signal_connect (G_OBJECT (self), "draw",
+                    G_CALLBACK (draw_cb), self);
+  g_signal_connect (G_OBJECT (self), "enter-notify-event",
+                    G_CALLBACK (on_crossing),  self);
+  g_signal_connect (G_OBJECT(self), "leave-notify-event",
+                    G_CALLBACK (on_crossing),  self);
+  g_signal_connect (G_OBJECT(self->drag), "drag-update",
+                    G_CALLBACK (drag_update),  self);
+  g_signal_connect (G_OBJECT(self->drag), "drag-end",
+                    G_CALLBACK (drag_end),  self);
+  return self;
+}
+
+static void
+knob_widget_init (KnobWidget * self)
+{
+  /* make it able to notify */
+  gtk_widget_set_has_window (GTK_WIDGET (self), TRUE);
+  int crossing_mask = GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK;
+  gtk_widget_add_events (GTK_WIDGET (self), crossing_mask);
+  self->drag = GTK_GESTURE_DRAG (gtk_gesture_drag_new (GTK_WIDGET (&self->parent_instance)));
+}
+
+static void
+knob_widget_class_init (KnobWidgetClass * klass)
+{
+  knob_signals[VALUE_CHANGED] =
+    g_signal_new ("value-changed",
+                  G_TYPE_FROM_CLASS(G_OBJECT_CLASS(klass)),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET(KnobWidgetClass, value_changed),
+                  NULL, NULL,
+		  NULL,
+		  G_TYPE_NONE, 0);
+}
diff --git a/plugin/widgets/knob.h b/plugin/widgets/knob.h
new file mode 100644
index 0000000..c9ef71f
--- /dev/null
+++ b/plugin/widgets/knob.h
@@ -0,0 +1,72 @@
+/*
+ * gui/widgets/knob.h - knob
+ *
+ * Copyright (C) 2018 Alexandros Theodotou
+ *
+ * This file is part of Zrythm
+ *
+ * Zrythm is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Zrythm is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Zrythm.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/** \file
+ */
+
+#ifndef __GUI_WIDGETS_KNOB_H__
+#define __GUI_WIDGETS_KNOB_H__
+
+#include <gtk/gtk.h>
+
+#define KNOB_WIDGET_TYPE          (knob_widget_get_type ())
+#define KNOB_WIDGET(obj)                  (G_TYPE_CHECK_INSTANCE_CAST ((obj), KNOB_WIDGET_TYPE, Knob))
+#define KNOB_WIDGET_CLASS(klass)          (G_TYPE_CHECK_CLASS_CAST  ((klass), KNOB_WIDGET, KnobWidgetClass))
+#define IS_KNOB_WIDGET(obj)               (G_TYPE_CHECK_INSTANCE_TYPE ((obj), KNOB_WIDGET_TYPE))
+#define IS_KNOB_WIDGET_CLASS(klass)       (G_TYPE_CHECK_CLASS_TYPE  ((klass), KNOB_WIDGET_TYPE))
+#define KNOB_WIDGET_GET_CLASS(obj)        (G_TYPE_INSTANCE_GET_CLASS  ((obj), KNOB_WIDGET_TYPE, KnobWidgetClass))
+
+typedef struct KnobWidget
+{
+  GtkDrawingArea        parent_instance;
+  float                 * value;       ///< value to bind to.
+                              ///< this value will get updated as the knob turns
+  int                   size;  ///< size in px
+  int                   hover;   ///< used to detect if hovering or not
+  float                 zero;   ///<   zero point 0.0-1.0 */
+  int                   arc;    ///< draw arc around the knob or not
+  int                   bevel;  ///< bevel
+  int                   flat;    ///< flat or 3D
+  GdkColor              start_color;    ///< color away from zero point
+  GdkColor              end_color;     ///< color close to zero point
+  GtkGestureDrag        *drag;     ///< used for drag gesture
+  double                last_x;    ///< used in gesture drag
+  double                last_y;    ///< used in gesture drag
+} KnobWidget;
+
+typedef struct KnobWidgetClass
+{
+  GtkDrawingAreaClass   parent_class;
+
+  void (* value_changed)(KnobWidget *);
+} KnobWidgetClass;
+
+/**
+ * Creates a knob widget with the given options and binds it to the given value.
+ */
+KnobWidget *
+knob_widget_new (float     * value,
+                 int         size,
+                 float       zero);
+
+
+
+#endif
-- 
GitLab