diff --git a/Makefile b/Makefile index 9608f3cb2..69d02e6dd 100644 --- a/Makefile +++ b/Makefile @@ -138,6 +138,9 @@ install: # Create directories install -d $(DESTDIR)$(PREFIX)/bin/ install -d $(DESTDIR)$(PREFIX)/lib/carla/ + install -d $(DESTDIR)$(PREFIX)/lib/carla/resources/ + install -d $(DESTDIR)$(PREFIX)/lib/carla/resources/nekofilter/ + install -d $(DESTDIR)$(PREFIX)/lib/carla/resources/zynaddsubfx/ install -d $(DESTDIR)$(PREFIX)/share/applications/ install -d $(DESTDIR)$(PREFIX)/share/icons/hicolor/16x16/apps/ install -d $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/ @@ -187,6 +190,11 @@ install: # Install python code install -m 755 source/*.py $(DESTDIR)$(PREFIX)/share/carla/ + # Install resources + install -m 644 source/backend/resources/nekofilter-ui $(DESTDIR)$(PREFIX)/lib/carla/resources/ + install -m 644 source/backend/resources/nekofilter/*.png $(DESTDIR)$(PREFIX)/lib/carla/resources/nekofilter/ + install -m 644 source/backend/resources/zynaddsubfx/*.png $(DESTDIR)$(PREFIX)/lib/carla/resources/zynaddsubfx/ + # Adjust PREFIX value in script files sed -i "s/X-PREFIX-X/$(SED_PREFIX)/" \ $(DESTDIR)$(PREFIX)/bin/carla diff --git a/source/backend/resources/nekofilter-ui b/source/backend/resources/nekofilter-ui new file mode 100755 index 000000000..2ad10a689 --- /dev/null +++ b/source/backend/resources/nekofilter-ui @@ -0,0 +1,1243 @@ +#!/usr/bin/env python2 +# +# Copyright (C) 2008,2009 Nedko Arnaudov +# Copyright (C) 2006 Leonard Ritter +# Filter response code by Fons Adriaensen +# +# This program 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; version 2 of the License +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + +import sys +import os +import fcntl +import gtk +import gobject +import cairo +from math import pi, sin, cos, atan2, log, sqrt, hypot, log10 +from colorsys import hls_to_rgb, rgb_to_hls + +def map_coords_linear(x,y): + return x,1.0-y + +def map_coords_spheric(x,y): + nx = cos(x * 2 * pi) * y + ny = -sin(x * 2 * pi) * y + return nx, ny + +def get_peaks(f, tolerance=0.01, maxd=0.01, mapfunc=map_coords_linear): + corners = 360 + yc = 1.0/corners + peaks = [] + x0,y0 = 0.0,0.0 + t0 = -9999.0 + i0 = 0 + for i in xrange(int(corners)): + p = i*yc + a = f(p) + x,y = mapfunc(p, a) + if i == 0: + x0,y0 = x,y + t = atan2((y0 - y), (x0 - x)) / (2*pi) + td = t - t0 + if (abs(td) >= tolerance): + t0 = t + peaks.append((x,y)) + x0,y0 = x,y + return peaks + +def make_knobshape(gaps, gapdepth): + def knobshape_func(x): + x = (x*gaps)%1.0 + w = 0.5 + g1 = 0.5 - w*0.5 + g2 = 0.5 + w*0.5 + if (x >= g1) and (x < 0.5): + x = (x-g1)/(w*0.5) + return 0.5 - gapdepth * x * 0.9 + elif (x >= 0.5) and (x < g2): + x = (x-0.5)/(w*0.5) + return 0.5 - gapdepth * (1-x) * 0.9 + else: + return 0.5 + return get_peaks(knobshape_func, 0.03, 0.05, map_coords_spheric) + +def hls_to_color(h,l,s): + r,g,b = hls_to_rgb(h,l,s) + return gtk.gdk.color_parse('#%04X%04X%04X' % (int(r*65535),int(g*65535),int(b*65535))) + +def color_to_hls(color): + string = color.to_string() + r = int(string[1:5], 16) / 65535.0 + g = int(string[5:9], 16) / 65535.0 + b = int(string[9:13], 16) / 65535.0 + return rgb_to_hls(r, g, b) + +MARKER_NONE = '' +MARKER_LINE = 'line' +MARKER_ARROW = 'arrow' +MARKER_DOT = 'dot' + +LEGEND_NONE = '' +LEGEND_DOTS = 'dots' # painted dots +LEGEND_LINES = 'lines' # painted ray-like lines +LEGEND_RULER = 'ruler' # painted ray-like lines + a circular one +LEGEND_RULER_INWARDS = 'ruler-inwards' # same as ruler, but the circle is on the outside +LEGEND_LED_SCALE = 'led-scale' # an LCD scale +LEGEND_LED_DOTS = 'led-dots' # leds around the knob + +class KnobTooltip: + def __init__(self): + self.tooltip_window = gtk.Window(gtk.WINDOW_POPUP) + self.tooltip = gtk.Label() + #self.tooltip.modify_fg(gtk.STATE_NORMAL, hls_to_color(0.0, 1.0, 0.0)) + self.tooltip_timeout = None + vbox = gtk.VBox() + vbox2 = gtk.VBox() + vbox2.add(self.tooltip) + vbox2.set_border_width(2) + vbox.add(vbox2) + self.tooltip_window.add(vbox) + vbox.connect('expose-event', self.on_tooltip_expose) + + def show_tooltip(self, knob): + text = knob.format_value() + rc = knob.get_allocation() + x,y = knob.window.get_origin() + self.tooltip_window.show_all() + w,h = self.tooltip_window.get_size() + wx,wy = x+rc.x-w, y+rc.y+rc.height/2-h/2 + self.tooltip_window.move(wx,wy) + rc = self.tooltip_window.get_allocation() + self.tooltip_window.window.invalidate_rect((0,0,rc.width,rc.height), False) + self.tooltip.set_text(text) + if self.tooltip_timeout: + gobject.source_remove(self.tooltip_timeout) + self.tooltip_timeout = gobject.timeout_add(500, self.hide_tooltip) + + def hide_tooltip(self): + self.tooltip_window.hide_all() + + def on_tooltip_expose(self, widget, event): + ctx = widget.window.cairo_create() + rc = widget.get_allocation() + #ctx.set_source_rgb(*hls_to_rgb(0.0, 0.0, 0.5)) + #ctx.paint() + ctx.set_source_rgb(*hls_to_rgb(0.0, 0.0, 0.5)) + ctx.translate(0.5, 0.5) + ctx.set_line_width(1) + ctx.rectangle(rc.x, rc.y,rc.width-1,rc.height-1) + ctx.stroke() + return False + + + +knob_tooltip = None +def get_knob_tooltip(): + global knob_tooltip + if not knob_tooltip: + knob_tooltip = KnobTooltip() + return knob_tooltip + +class SmartAdjustment(gtk.Adjustment): + def __init__(self, log=False, value=0, lower=0, upper=0, step_incr=0, page_incr=0, page_size=0): + self.log = log + gtk.Adjustment.__init__(self, value, lower, upper, step_incr, page_incr, page_size) + self.normalized_value = self.real2norm(self.value) + + def real2norm(self, value): + if self.log: + return log(value / self.lower, self.upper / self.lower) + else: + return (value - self.lower) / (self.upper - self.lower) + + def norm2real(self, value): + if self.log: + return self.lower * pow(self.upper / self.lower, value) + else: + return value * (self.upper - self.lower) + self.lower + + def set_value(self, value): + self.normalized_value = self.real2norm(value) + gtk.Adjustment.set_value(self, value) + + def get_normalized_value(self): + return self.normalized_value + + def set_normalized_value(self, value): + self.normalized_value = value + + if self.normalized_value < 0.0: + self.normalized_value = 0.0 + elif self.normalized_value > 1.0: + self.normalized_value = 1.0 + + self.set_value(self.norm2real(self.normalized_value)) + +class Knob(gtk.VBox): + def __init__(self): + gtk.VBox.__init__(self) + self.gapdepth = 4 + self.gaps = 10 + self.value = 0.0 + self.min_value = 0.0 + self.max_value = 127.0 + self.fg_hls = 0.0, 0.7, 0.0 + self.legend_hls = None + self.dragging = False + self.start = 0.0 + self.digits = 2 + self.segments = 13 + self.label = '' + self.marker = MARKER_LINE + self.angle = (3.0/4.0) * 2 * pi + self.knobshape = None + self.legend = LEGEND_DOTS + self.lsize = 2 + self.lscale = False + self.set_double_buffered(True) + self.connect('realize', self.on_realize) + self.connect("size_allocate", self.on_size_allocate) + self.connect('expose-event', self.on_expose) + self.set_border_width(6) + self.set_size_request(50, 50) + self.tooltip_enabled = False + self.adj = None + + def set_adjustment(self, adj): + self.min_value = 0.0 + self.max_value = 1.0 + self.value = adj.get_normalized_value() + if self.adj: + self.adj.disconnect(self.adj_id) + self.adj = adj + self.adj_id = adj.connect("value-changed", self.on_adj_value_changed) + + def is_sensitive(self): + return self.get_property("sensitive") + + def format_value(self): + if self.adj: + value = self.adj.value + else: + value = self.value + return ("%%.%if" % self.digits) % value + + def show_tooltip(self): + if self.tooltip_enabled: + get_knob_tooltip().show_tooltip(self) + + def on_realize(self, widget): + self.root = self.get_toplevel() + self.root.add_events(gtk.gdk.ALL_EVENTS_MASK) + self.root.connect('scroll-event', self.on_mousewheel) + self.root.connect('button-press-event', self.on_left_down) + self.root.connect('button-release-event', self.on_left_up) + self.root.connect('motion-notify-event', self.on_motion) + self.update_knobshape() + + def update_knobshape(self): + rc = self.get_allocation() + b = self.get_border_width() + size = min(rc.width, rc.height) - 2*b + gd = float(self.gapdepth*0.5) / size + self.gd = gd + self.knobshape = make_knobshape(self.gaps, gd) + + def set_legend_scale(self, scale): + self.lscale = scale + self.refresh() + + def set_legend_line_width(self, width): + self.lsize = width + self.refresh() + + def set_segments(self, segments): + self.segments = segments + self.refresh() + + def set_marker(self, marker): + self.marker = marker + self.refresh() + + def set_range(self, minvalue, maxvalue): + self.min_value = minvalue + self.max_value = maxvalue + self.set_value(self.value) + + def quantize_value(self, value): + scaler = 10**self.digits + value = int((value*scaler)+0.5) / float(scaler) + return value + + def on_adj_value_changed(self, adj): + new_value = adj.get_normalized_value() + if self.value != new_value: + self.value = new_value + self.refresh() + + def set_value(self, value): + oldval = self.value + self.value = min(max(self.quantize_value(value), self.min_value), self.max_value) + if self.value != oldval: + if self.adj: + self.adj.set_normalized_value(value) + self.refresh() + + def get_value(self): + return self.value + + def set_top_color(self, h, l, s): + self.fg_hls = h,l,s + self.refresh() + + def set_legend_color(self, h, l, s): + self.legend_hls = h,l,s + self.refresh() + + def get_top_color(self): + return self.fg_hls + + def set_gaps(self, gaps): + self.gaps = gaps + self.knobshape = None + self.refresh() + + def get_gaps(self): + return self.gaps + + def set_gap_depth(self, gapdepth): + self.gapdepth = gapdepth + self.knobshape = None + self.refresh() + + def get_gap_depth(self): + return self.gapdepth + + def set_angle(self, angle): + self.angle = angle + self.refresh() + + def get_angle(self): + return self.angle + + def set_legend(self, legend): + self.legend = legend + self.refresh() + + def get_legend(self): + return self.legend + + def on_left_down(self, widget, event): + #print("on_left_down") + + # dont drag insensitive widgets + if not self.is_sensitive(): + return False + + if not sum(self.get_allocation().intersect((int(event.x), int(event.y), 1, 1))): + return False + if event.button == 1: + #print("start draggin") + self.startvalue = self.value + self.start = event.y + self.dragging = True + self.show_tooltip() + self.grab_add() + return True + return False + + def on_left_up(self, widget, event): + #print("on_left_up") + if not self.dragging: + return False + if event.button == 1: + #print("stop draggin") + self.dragging = False + self.grab_remove() + return True + return False + + def on_motion(self, widget, event): + #print("on_motion") + + # dont drag insensitive widgets + if not self.is_sensitive(): + return False + + if self.dragging: + x,y,state = self.window.get_pointer() + rc = self.get_allocation() + range = self.max_value - self.min_value + scale = rc.height + if event.state & gtk.gdk.SHIFT_MASK: + scale = rc.height*8 + value = self.startvalue - ((y - self.start)*range)/scale + oldval = self.value + self.set_value(value) + self.show_tooltip() + if oldval != self.value: + self.start = y + self.startvalue = self.value + return True + return False + + def on_mousewheel(self, widget, event): + + # dont move insensitive widgets + if not self.is_sensitive(): + return False + + if not sum(self.get_allocation().intersect((int(event.x), int(event.y), 1, 1))): + return + range = self.max_value - self.min_value + minstep = 1.0 / (10**self.digits) + if event.state & (gtk.gdk.SHIFT_MASK | gtk.gdk.BUTTON1_MASK): + step = minstep + else: + step = max(self.quantize_value(range/25.0), minstep) + value = self.value + if event.direction == gtk.gdk.SCROLL_UP: + value += step + elif event.direction == gtk.gdk.SCROLL_DOWN: + value -= step + self.set_value(value) + self.show_tooltip() + + def on_size_allocate(self, widget, allocation): + #print(allocation.x, allocation.y, allocation.width, allocation.height) + self.update_knobshape() + + def draw_points(self, ctx, peaks): + ctx.move_to(*peaks[0]) + for peak in peaks[1:]: + ctx.line_to(*peak) + + def draw(self, ctx): + if not self.legend_hls: + self.legend_hls = color_to_hls(self.style.fg[gtk.STATE_NORMAL]) + + if not self.knobshape: + self.update_knobshape() + startangle = pi*1.5 - self.angle*0.5 + angle = ((self.value - self.min_value) / (self.max_value - self.min_value)) * self.angle + startangle + rc = self.get_allocation() + size = min(rc.width, rc.height) + + kh = self.get_border_width() # knob height + + ps = 1.0/size # pixel size + ps2 = 1.0 / (size-(2*kh)-1) # pixel size inside knob + ss = ps * kh # shadow size + lsize = ps2 * self.lsize # legend line width + # draw spherical + ctx.translate(rc.x, rc.y) + ctx.translate(0.5,0.5) + ctx.translate(size*0.5, size*0.5) + ctx.scale(size-(2*kh)-1, size-(2*kh)-1) + if self.legend == LEGEND_DOTS: + ctx.save() + ctx.set_source_rgb(*hls_to_rgb(*self.legend_hls)) + dots = self.segments + for i in xrange(dots): + s = float(i)/(dots-1) + a = startangle + self.angle*s + ctx.save() + ctx.rotate(a) + r = lsize*0.5 + if self.lscale: + r = max(r*s,ps2) + ctx.arc(0.5+lsize, 0.0, r, 0.0, 2*pi) + ctx.fill() + ctx.restore() + ctx.restore() + elif self.legend in (LEGEND_LINES, LEGEND_RULER, LEGEND_RULER_INWARDS): + ctx.save() + ctx.set_source_rgb(*hls_to_rgb(*self.legend_hls)) + dots = self.segments + n = ps2*(kh-1) + for i in xrange(dots): + s = float(i)/(dots-1) + a = startangle + self.angle*s + ctx.save() + ctx.rotate(a) + r = n*0.9 + if self.lscale: + r = max(r*s,ps2) + ctx.move_to(0.5+ps2+n*0.1, 0.0) + ctx.line_to(0.5+ps2+n*0.1+r, 0.0) + ctx.set_line_width(lsize) + ctx.stroke() + ctx.restore() + ctx.restore() + if self.legend == LEGEND_RULER: + ctx.save() + ctx.set_source_rgb(*hls_to_rgb(*self.legend_hls)) + ctx.set_line_width(lsize) + ctx.arc(0.0, 0.0, 0.5+ps2+n*0.1, startangle, startangle+self.angle) + ctx.stroke() + ctx.restore() + elif self.legend == LEGEND_RULER_INWARDS: + ctx.save() + ctx.set_source_rgb(*hls_to_rgb(*self.legend_hls)) + ctx.set_line_width(lsize) + ctx.arc(0.0, 0.0, 0.5+ps2+n, startangle, startangle+self.angle) + ctx.stroke() + + # draw shadow only for sensitive widgets that have height + if self.is_sensitive() and kh: + ctx.save() + ctx.translate(ss, ss) + ctx.rotate(angle) + self.draw_points(ctx, self.knobshape) + ctx.close_path() + ctx.restore() + ctx.set_source_rgba(0,0,0,0.3) + ctx.fill() + + if self.legend in (LEGEND_LED_SCALE, LEGEND_LED_DOTS): + ch,cl,cs = self.legend_hls + n = ps2*(kh-1) + ctx.save() + ctx.set_line_cap(cairo.LINE_CAP_ROUND) + ctx.set_source_rgb(*hls_to_rgb(ch,cl*0.2,cs)) + ctx.set_line_width(lsize) + ctx.arc(0.0, 0.0, 0.5+ps2+n*0.5, startangle, startangle+self.angle) + ctx.stroke() + ctx.set_source_rgb(*hls_to_rgb(ch,cl,cs)) + if self.legend == LEGEND_LED_SCALE: + ctx.set_line_width(lsize-ps2*2) + ctx.arc(0.0, 0.0, 0.5+ps2+n*0.5, startangle, angle) + ctx.stroke() + elif self.legend == LEGEND_LED_DOTS: + dots = self.segments + dsize = lsize-ps2*2 + seg = self.angle/dots + endangle = startangle + self.angle + for i in xrange(dots): + s = float(i)/(dots-1) + a = startangle + self.angle*s + if ((a-seg*0.5) > angle) or (angle == startangle): + break + ctx.save() + ctx.rotate(a) + r = dsize*0.5 + if self.lscale: + r = max(r*s,ps2) + ctx.arc(0.5+ps2+n*0.5, 0.0, r, 0.0, 2*pi) + ctx.fill() + ctx.restore() + ctx.restore() + pat = cairo.LinearGradient(-0.5, -0.5, 0.5, 0.5) + pat.add_color_stop_rgb(1.0, 0.2,0.2,0.2) + pat.add_color_stop_rgb(0.0, 0.3,0.3,0.3) + ctx.set_source(pat) + ctx.rotate(angle) + self.draw_points(ctx, self.knobshape) + ctx.close_path() + ctx.fill_preserve() + ctx.set_source_rgba(0.1,0.1,0.1,1) + ctx.save() + ctx.identity_matrix() + ctx.set_line_width(1.0) + ctx.stroke() + ctx.restore() + + ctx.arc(0.0, 0.0, 0.5-self.gd, 0.0, pi*2.0) + ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], max(self.fg_hls[1]*0.4,0.0), self.fg_hls[2])) + ctx.fill() + ctx.arc(0.0, 0.0, 0.5-self.gd-ps, 0.0, pi*2.0) + ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], min(self.fg_hls[1]*1.2,1.0), self.fg_hls[2])) + ctx.fill() + ctx.arc(0.0, 0.0, 0.5-self.gd-(2*ps), 0.0, pi*2.0) + ctx.set_source_rgb(*hls_to_rgb(*self.fg_hls)) + ctx.fill() + + # dont draw cap for insensitive widgets + if not self.is_sensitive(): + return + + #~ ctx.set_line_cap(cairo.LINE_CAP_ROUND) + #~ ctx.move_to(0.5-0.3-self.gd-ps, 0.0) + #~ ctx.line_to(0.5-self.gd-ps*5, 0.0) + + if self.marker == MARKER_LINE: + ctx.set_line_cap(cairo.LINE_CAP_BUTT) + ctx.move_to(0.5-0.3-self.gd-ps, 0.0) + ctx.line_to(0.5-self.gd-ps, 0.0) + ctx.save() + ctx.identity_matrix() + ctx.translate(0.5,0.5) + ctx.set_line_width(5) + ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], min(self.fg_hls[1]*1.2,1.0), self.fg_hls[2])) + ctx.stroke_preserve() + ctx.set_line_width(3) + ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], max(self.fg_hls[1]*0.4,0.0), self.fg_hls[2])) + ctx.stroke() + ctx.restore() + elif self.marker == MARKER_DOT: + ctx.arc(0.5-0.05-self.gd-ps*5, 0.0, 0.05, 0.0, 2*pi) + ctx.save() + ctx.identity_matrix() + ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], min(self.fg_hls[1]*1.2,1.0), self.fg_hls[2])) + ctx.stroke_preserve() + ctx.set_line_width(1) + ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], max(self.fg_hls[1]*0.4,0.0), self.fg_hls[2])) + ctx.fill() + ctx.restore() + elif self.marker == MARKER_ARROW: + ctx.set_line_cap(cairo.LINE_CAP_BUTT) + ctx.move_to(0.5-0.3-self.gd-ps, 0.1) + ctx.line_to(0.5-0.1-self.gd-ps, 0.0) + ctx.line_to(0.5-0.3-self.gd-ps, -0.1) + ctx.close_path() + ctx.save() + ctx.identity_matrix() + #~ ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], min(self.fg_hls[1]*1.2,1.0), self.fg_hls[2])) + #~ ctx.stroke_preserve() + ctx.set_line_width(1) + ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], max(self.fg_hls[1]*0.4,0.0), self.fg_hls[2])) + ctx.fill() + ctx.restore() + + def refresh(self): + rect = self.get_allocation() + if self.window: + self.window.invalidate_rect(rect, False) + return True + + def on_expose(self, widget, event): + self.context = self.window.cairo_create() + self.draw(self.context) + return False + +class filter_band: + def __init__(self, sample_rate): + self.fsamp = sample_rate + + def set_params(self, freq, bandw, gain): + freq_ratio = freq / self.fsamp + gain2 = pow(10.0, 0.05 * gain) + b = 7 * bandw * freq_ratio / sqrt(gain2) + self.gn = 0.5 * (gain2 - 1) + self.v1 = -cos(2 * pi * freq_ratio) + self.v2 = (1 - b) / (1 + b) + self.v1 *= (1 + self.v2) + self.gn *= (1 - self.v2) + + def get_response(self, freq): + w = 2 * pi * (freq / self.fsamp) + c1 = cos(w) + s1 = sin(w) + c2 = cos(2 * w) + s2 = sin(2 * w) + + x = c2 + self.v1 * c1 + self.v2 + y = s2 + self.v1 * s1 + t1 = hypot(x, y) + x += self.gn * (c2 - 1) + y += self.gn * s2 + t2 = hypot(x, y) + + #return t2 / t1 + return 20 * log10(t2 / t1) + +class frequency_response(gtk.DrawingArea): + def __init__(self): + gtk.DrawingArea.__init__(self) + + self.connect("expose-event", self.on_expose) + self.connect("size-request", self.on_size_request) + self.connect("size_allocate", self.on_size_allocate) + + self.color_bg = gtk.gdk.Color(0,0,0) + self.color_value = gtk.gdk.Color(int(65535 * 0.8), int(65535 * 0.7), 0) + self.color_mark = gtk.gdk.Color(int(65535 * 0.3), int(65535 * 0.3), int(65535 * 0.3)) + self.color_sum = gtk.gdk.Color(int(65535 * 1.0), int(65535 * 1.0), int(65535 * 1.0)) + self.width = 0 + self.height = 0 + self.margin = 10 + self.db_range = 30 + self.master_gain = 0.0 + self.master_enabled = False + + self.filters = {} + + def on_expose(self, widget, event): + cairo_ctx = widget.window.cairo_create() + + # set a clip region for the expose event + cairo_ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) + cairo_ctx.clip() + + self.draw(cairo_ctx) + + return False + + def on_size_allocate(self, widget, allocation): + #print(allocation.x, allocation.y, allocation.width, allocation.height) + self.width = float(allocation.width) + self.height = float(allocation.height) + self.font_size = 10 + + def on_size_request(self, widget, requisition): + #print("size-request, %u x %u" % (requisition.width, requisition.height)) + requisition.width = 150 + requisition.height = 150 + return + + def invalidate_all(self): + self.queue_draw_area(0, 0, int(self.width), int(self.height)) + + def get_x(self, hz): + width = self.width - 3.5 * self.margin + #x = self.margin + width * (hz - 20) / (20000 - 20) + x = 2.5 * self.margin + width * log(hz / 20.0, 1000.0) + #print(x) + return x + + def get_freq(self, x): + width = self.width - 3.5 * self.margin + return 20 * pow(1000, (x - 2.5 * self.margin) / width) + + def get_y(self, db): + height = self.height - 2.5 * self.margin + y = self.margin + height * (self.db_range - db) / (self.db_range * 2) + #print(y) + return y + + def draw_db_grid(self, cairo_ctx, db): + x = self.get_x(20) + y = self.get_y(db) + cairo_ctx.move_to(x, y) + cairo_ctx.line_to(self.get_x(20000), y) + + if db % 10 == 0: + x -= 20 + y += 3 + cairo_ctx.move_to(x, y) + label = "%+d" % db + if db == 0: + label = " " + label + cairo_ctx.show_text(label) + + cairo_ctx.stroke() + + def invalidate_all(self): + self.queue_draw_area(0, 0, int(self.width), int(self.height)) + + def draw(self, cairo_ctx): + cairo_ctx.select_font_face("Fixed") + + cairo_ctx.set_source_color(self.color_bg) + cairo_ctx.rectangle(0, 0, self.width, self.height) + cairo_ctx.fill() + + cairo_ctx.set_source_color(self.color_mark) + cairo_ctx.set_line_width(1); + + for hz in range(20, 101, 10) + range(100, 1001, 100) + range(1000, 10001, 1000) + range(10000, 20001, 10000): + if hz >= 10000: + label = "%dk" % int(hz / 1000) + elif hz >= 1000: + label = "%dk" % int(hz / 1000) + else: + label = "%d" % int(hz) + first_digit = int(label[0]) + if first_digit > 5 or (first_digit > 3 and (len(label) == 3)): + label = None + + x = self.get_x(hz) + cairo_ctx.move_to(x, self.get_y(self.db_range)) + y = self.get_y(-self.db_range) + cairo_ctx.line_to(x, y) + if label: + y += 10 + if hz == 20000: + x -= 15 + elif hz != 20: + x -= 3 + cairo_ctx.move_to(x, y) + cairo_ctx.show_text(label) + cairo_ctx.stroke() + + for db in range(0, self.db_range + 1, 5): + self.draw_db_grid(cairo_ctx, db) + + if db != 0: + self.draw_db_grid(cairo_ctx, -db) + + curves = [[x, {}, self.master_gain, self.get_freq(x)] for x in range(int(self.get_x(20)), int(self.get_x(20e3)))] + #print(repr(curves)) + + # calculate filter responses + for label, filter in self.filters.items(): + if not filter.enabled: + continue + + for point in curves: + db = filter.get_response(point[3]) + point[1][label] = [self.get_y(db), db] + + # calculate sum curve + for point in curves: + for label, filter_point in point[1].items(): + point[2] += filter_point[1] + #print(point) + + # draw filter curves + for label, filter in self.filters.items(): + if not filter.enabled: + continue + + cairo_ctx.set_source_color(filter.color) + cairo_ctx.move_to(curves[0][0], curves[0][1][label][0]) + for point in curves: + cairo_ctx.line_to(point[0], point[1][label][0]) + cairo_ctx.stroke() + + if self.master_enabled: + # draw sum curve + cairo_ctx.set_source_color(self.color_sum) + cairo_ctx.set_line_width(2); + cairo_ctx.move_to(curves[0][0], self.get_y(curves[0][2])) + for point in curves: + cairo_ctx.line_to(point[0], self.get_y(point[2])) + cairo_ctx.stroke() + + # draw base point markers + for label, filter in self.filters.items(): + if not filter.enabled: + continue + + cairo_ctx.set_source_color(self.color_value) + x = self.get_x(filter.adj_hz.value) + y = self.get_y(filter.adj_db.value) + + cairo_ctx.move_to(x, y) + cairo_ctx.show_text(label) + cairo_ctx.stroke() + + def add_filter(self, label, sample_rate, adj_hz, adj_db, adj_bw, color): + #print("filter %s added (%.2f Hz, %.2f dB, %.2f bw)" % (label, adj_hz.value, adj_db.value, adj_bw.value)) + filter = filter_band(sample_rate) + filter.enabled = False + filter.label = label + filter.color = color + filter.set_params(adj_hz.value, adj_bw.value, adj_db.value) + adj_hz.filter = filter + adj_db.filter = filter + adj_bw.filter = filter + filter.adj_hz = adj_hz + filter.adj_db = adj_db + filter.adj_bw = adj_bw + adj_hz.connect("value-changed", self.on_value_change_request) + adj_db.connect("value-changed", self.on_value_change_request) + adj_bw.connect("value-changed", self.on_value_change_request) + self.filters[label] = filter + + def enable_filter(self, label): + filter = self.filters[label] + #print("filter %s enabled (%.2f Hz, %.2f dB, %.2f bw)" % (label, filter.adj_hz.value, filter.adj_db.value, filter.adj_bw.value)) + filter.enabled = True + self.invalidate_all() + + def disable_filter(self, label): + filter = self.filters[label] + #print("filter %s disabled (%.2f Hz, %.2f dB, %.2f bw)" % (label, filter.adj_hz.value, filter.adj_db.value, filter.adj_bw.value)) + filter.enabled = False + self.invalidate_all() + + def on_value_change_request(self, adj): + #print("adj changed") + adj.filter.set_params(adj.filter.adj_hz.value, adj.filter.adj_bw.value, adj.filter.adj_db.value) + self.invalidate_all() + + def master_enable(self): + self.master_enabled = True; + self.invalidate_all() + + def master_disable(self): + self.master_enabled = False; + self.invalidate_all() + + def set_master_gain(self, gain): + self.master_gain = gain; + self.invalidate_all() + +class filter_ui: + def __init__(self, argv): + self.fake = len(argv) == 1 + + if self.fake: + self.shown = False + self.sample_rate = 48e3 + else: + self.sample_rate = float(argv[1]) + self.recv_pipe_fd = int(argv[2]) + self.send_pipe_fd = int(argv[3]) + + oldflags = fcntl.fcntl(self.recv_pipe_fd, fcntl.F_GETFL) + fcntl.fcntl(self.recv_pipe_fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK) + + self.recv_pipe = os.fdopen(self.recv_pipe_fd, 'r') + self.send_pipe = os.fdopen(self.send_pipe_fd, 'w') + + self.port_base = 0 + self.nekologo = gtk.gdk.pixbuf_new_from_file(os.path.join(sys.path[0], "nekofilter", "lv2logo.png")) + + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + #self.window.set_size_request(600, 400) + self.window.set_title("NekoFilter (GUI)") + self.window.set_role("plugin_ui") + + self.top_vbox = gtk.VBox() + self.top_vbox.set_spacing(10) + + align = gtk.Alignment(0.5, 0.5, 1.0, 1.0) + align.set_padding(10, 10, 10, 10) + align.add(self.top_vbox) + + self.window.add(align) + + self.fr = frequency_response() + self.fr.set_size_request(400, 200) + + frame = gtk.Frame() + frame.set_shadow_type(gtk.SHADOW_ETCHED_OUT) + frame.add(self.fr) + + self.top_vbox.pack_start(frame, True, True) + + self.param_hbox = gtk.HBox() + self.top_vbox.pack_start(self.param_hbox) + + self.param_hbox.set_spacing(10) + + self.initator = False + + self.ports = [] + + misc_box = gtk.VBox() + misc_box.set_spacing(5) + + master_frame = gtk.Frame("Master") + master_frame.set_label_align(0.5, 0.5) + + master_box = gtk.VBox() + master_box.set_spacing(5) + + port = {'index': 0, 'name': 'Active', 'type': 'toggle'} + self.ports.append(port) + self.add_param_box(master_box, self.create_toggle_box(port)) + + port = {'index': 1, 'name': 'Gain', 'type': 'knob', 'min': -20.0, 'max': 20.0, 'unit': 'dB', 'log': False} + self.ports.append(port) + self.add_param_box(master_box, self.create_knob_box(port)) + + master_frame.add(master_box) + misc_box.pack_start(master_frame, False, False) + + logo = gtk.Image() + logo.set_from_pixbuf(self.nekologo) + misc_box.pack_start(logo, True, True) + + button_box = gtk.VBox() + + button = gtk.Button(stock=gtk.STOCK_ABOUT) + button.connect("clicked", self.on_about) + button_box.pack_start(button) + + button = gtk.Button(stock=gtk.STOCK_CLOSE) + button.connect("clicked", self.on_window_closed) + button_box.pack_start(button) + + align = gtk.Alignment(0.5, 1.0, 1.0, 0.0) + align.add(button_box) + misc_box.pack_start(align, True, True) + + band_parameters = [ + {'name': 'Active', 'type': 'toggle'}, + {'name': 'Frequency', 'type': 'knob', 'unit': 'Hz', 'log': True}, + {'name': 'Bandwidth', 'type': 'knob', 'min': 0.125, 'max': 8.0, 'unit': '', 'log': True}, + {'name': 'Gain', 'type': 'knob', 'min': -20.0, 'max': 20.0, 'unit': 'dB', 'log': False}] + + freq_min = [ 20.0, 40.0, 100.0, 200.0] + freq_max = [2000.0, 4000.0, 10000.0, 20000.0] + + port_index = 2 + + filter_colors = [gtk.gdk.Color(int(65535 * 1.0), int(65535 * 0.6), int(65535 * 0.0)), + gtk.gdk.Color(int(65535 * 0.6), int(65535 * 1.0), int(65535 * 0.6)), + gtk.gdk.Color(int(65535 * 0.0), int(65535 * 0.6), int(65535 * 1.0)), + gtk.gdk.Color(int(65535 * 0.9), int(65535 * 0.0), int(65535 * 0.5))] + + for i in [0, 1, 2, 3]: + band_frame = gtk.Frame("Band %d" % (i + 1)) + band_frame.set_label_align(0.5, 0.5) + + band_box = gtk.VBox() + band_box.set_spacing(5) + + for parameter in band_parameters: + + port = parameter.copy() + port['index'] = port_index + port_index += 1 + + if port['name'] == 'Frequency': + port['min'] = freq_min[i] + port['max'] = freq_max[i] + + self.ports.append(port) + + #param_box.set_spacing(5) + if port['type'] == 'knob': + self.add_param_box(band_box, self.create_knob_box(port)) + elif port['type'] == 'toggle': + self.add_param_box(band_box, self.create_toggle_box(port)) + + self.fr.add_filter( + str(i + 1), + self.sample_rate, + self.ports[port_index - 3]['adj'], # frequency + self.ports[port_index - 1]['adj'], # gain + self.ports[port_index - 2]['adj'], # bandwidth + filter_colors[i]) + + band_frame.add(band_box) + + self.param_hbox.pack_start(band_frame, True, True) + + self.param_hbox.pack_start(misc_box, True, True) + + self.initator = True + + def on_about(self, widget): + about = gtk.AboutDialog() + about.set_transient_for(self.window) + about.set_name("4-band parametric filter") + #about.set_website(program_data['website']) + about.set_authors(["Filipe Coelho - Carla port", + "Nedko Arnaudov - Original LV2 plugin and GUI", + "Fons Adriaensen - DSP code"]) + #about.set_artists(["LV2 logo has been designed by Thorsten Wilms, based on a concept from Peter Shorthose."]) + about.set_logo(self.nekologo) + about.show() + about.run() + about.hide() + + def create_knob_box(self, port): + param_box = gtk.VBox() + step = (port['max'] - port['min']) / 100 + adj = SmartAdjustment(port['log'], port['min'], port['min'], port['max'], step, step * 20) + adj.port = port + port['adj'] = adj + + adj.connect("value-changed", self.on_adj_value_changed) + + knob = Knob() + knob.set_adjustment(adj) + align = gtk.Alignment(0.5, 0.5, 0, 0) + align.set_padding(0, 0, 20, 20) + align.add(knob) + param_box.pack_start(align, False) + + adj.label = gtk.Label(self.get_adj_value_text(adj)[0]) + param_box.pack_start(adj.label, False) + #spin = gtk.SpinButton(adj, 0.0, 2) + #param_box.pack_start(spin, False) + + label = gtk.Label(port['name']) + param_box.pack_start(label, False) + return param_box + + def create_toggle_box(self, port): + param_box = gtk.VBox() + button = gtk.CheckButton(port['name']) + button.port = port + port['widget'] = button + + button.connect("toggled", self.on_button_toggled) + + align = gtk.Alignment(0.5, 0.5, 0, 0) + align.add(button) + param_box.pack_start(align, False) + return param_box + + def add_param_box(self, container, param_box): + align = gtk.Alignment(0.5, 0.5, 1.0, 1.0) + align.set_padding(10, 10, 10, 10) + align.add(param_box) + + container.pack_start(align, True) + + def get_adj_value_text(self, adj): + value = adj.get_value() + if value >= 10000: + format = "%.0f" + elif value >= 1000: + format = "%.1f" + else: + format = "%.2f" + text = format % value + unit = adj.port['unit'] + if unit: + text += " " + unit + + return value, text + + def on_adj_value_changed(self, adj): + value, text = self.get_adj_value_text(adj) + adj.label.set_text(text) + + if adj.port['index'] == 1: + #print("Master gain = %.2f dB" % adj.get_value()) + self.fr.set_master_gain(adj.get_value()) + + if self.initator: + #print(adj.port, adj.get_value()) + self.send_port_value(adj.port['index'] + self.port_base, value) + + def on_button_toggled(self, widget): + port_index = widget.port['index'] + band_no = (port_index - 2) / 4 + 1 + if widget.get_active(): + value = 1.0 + if band_no > 0: + self.fr.enable_filter(str(band_no)) + else: + self.fr.master_enable() + else: + value = 0.0 + if band_no > 0: + self.fr.disable_filter(str(band_no)) + else: + self.fr.master_disable() + + if self.initator: + self.send_port_value(port_index + self.port_base, value) + + def send(self, lines): + if self.fake: + return + + for line in lines: + #print('send: "' + line + '"') + self.send_pipe.write(line + "\n") + self.send_pipe.flush() + + def send_close(self): + self.send(["close"]) + + def send_exiting(self): + self.send(["exiting"]) + + def send_port_value(self, port_index, value): + self.send(["port_value", str(port_index), "%.10f" % value]) + + def send_hi(self): + self.send([""]) # send empty line (just newline char) + + def recv_line(self): + return self.recv_pipe.readline().strip() + + def recv_command(self): + try: + msg = self.recv_line() + + if msg == "port_value": + port_index = int(self.recv_line()) + port_value = float(self.recv_line()) + #print("port_value_change recevied: %d %f" % (port_index, port_value)) + self.on_port_value_changed(port_index, port_value) + elif msg == "show": + self.on_show() + elif msg == "hide": + self.on_hide() + elif msg == "quit": + self.on_quit() + else: + print("unknown message: \"" + msg + "\"") + + return True + except IOError: + return False + + def on_recv(self, fd, cond): + #print("on_recv") + if cond == gobject.IO_HUP: + gtk.main_quit() + return False + + while True: + if not self.recv_command(): + break + + return True + + def run(self): + self.window.connect("destroy", self.on_window_closed) + + if self.fake: + if not self.shown: + self.shown = True + self.on_show() + else: + self.send_hi() + gobject.io_add_watch(self.recv_pipe_fd, gobject.IO_IN | gobject.IO_HUP, self.on_recv) + + gtk.main() + + def on_port_value_changed(self, port_index, port_value): + #print("port %d set to %f" % (port_index, port_value)) + port_index -= self.port_base + port = self.ports[port_index] + #print(repr(port)) + port_type = port['type'] + if port_type == 'knob': + self.initator = False + port['adj'].set_value(port_value) + self.initator = True + elif port_type == 'toggle': + if port_value > 0.0: + toggled = True + else: + toggled = False + + self.initator = False + port['widget'].set_active(toggled) + self.initator = True + + def on_show(self): + self.window.show_all() + + def on_hide(self): + self.window.hide_all() + + def on_quit(self): + gtk.main_quit() + + def on_window_closed(self, arg): + self.window.hide_all() + self.send_close() + #self.send_exiting() + + if self.fake: + gtk.main_quit() + +def main(): + filter_ui(sys.argv).run() + #print("main done") + +if __name__ == '__main__': + main() diff --git a/source/backend/resources/nekofilter/lv2logo.png b/source/backend/resources/nekofilter/lv2logo.png new file mode 100644 index 000000000..fc2a1e1e8 Binary files /dev/null and b/source/backend/resources/nekofilter/lv2logo.png differ diff --git a/source/backend/resources/zynaddsubfx/black_key.png b/source/backend/resources/zynaddsubfx/black_key.png new file mode 100644 index 000000000..d90b6097f Binary files /dev/null and b/source/backend/resources/zynaddsubfx/black_key.png differ diff --git a/source/backend/resources/zynaddsubfx/black_key_pressed.png b/source/backend/resources/zynaddsubfx/black_key_pressed.png new file mode 100644 index 000000000..e33750db1 Binary files /dev/null and b/source/backend/resources/zynaddsubfx/black_key_pressed.png differ diff --git a/source/backend/resources/zynaddsubfx/knob.png b/source/backend/resources/zynaddsubfx/knob.png new file mode 100644 index 000000000..2d50139d9 Binary files /dev/null and b/source/backend/resources/zynaddsubfx/knob.png differ diff --git a/source/backend/resources/zynaddsubfx/module_backdrop.png b/source/backend/resources/zynaddsubfx/module_backdrop.png new file mode 100644 index 000000000..3ce1b2557 Binary files /dev/null and b/source/backend/resources/zynaddsubfx/module_backdrop.png differ diff --git a/source/backend/resources/zynaddsubfx/white_key.png b/source/backend/resources/zynaddsubfx/white_key.png new file mode 100644 index 000000000..e9125f290 Binary files /dev/null and b/source/backend/resources/zynaddsubfx/white_key.png differ diff --git a/source/backend/resources/zynaddsubfx/white_key_pressed.png b/source/backend/resources/zynaddsubfx/white_key_pressed.png new file mode 100644 index 000000000..f4d08470e Binary files /dev/null and b/source/backend/resources/zynaddsubfx/white_key_pressed.png differ diff --git a/source/backend/resources/zynaddsubfx/window_backdrop.png b/source/backend/resources/zynaddsubfx/window_backdrop.png new file mode 100644 index 000000000..8492b5f07 Binary files /dev/null and b/source/backend/resources/zynaddsubfx/window_backdrop.png differ