Commit 5fd1ec15 authored by ale's avatar ale

Start adding humanization UI controls

parent bc4080a9
......@@ -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)
......
......@@ -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);
......
......@@ -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;
}
......
......@@ -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
......@@ -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);
}
......
/*
* 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);
}
/*
* 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);