root/ui

Revision 6c00179c26506e9c6c29dafe0367a722d38a0e23, 40.3 KB (checked in by Nedko Arnaudov <nedko@…>, 3 years ago)

Detect UI launch failures and clean the mess

This often happens because of missing runtime dependencies
that python ui script needs (pygtk, pycairo, etc)

  • Property mode set to 100755
Line 
1#!/usr/bin/env python
2#
3# Copyright (C) 2008,2009 Nedko Arnaudov <nedko@arnaudov.name>
4# Copyright (C) 2006 Leonard Ritter <contact@leonard-ritter.com>
5# Filter response code by Fons Adriaensen
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; version 2 of the License
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
19
20import sys
21import os
22import fcntl
23import gtk
24import gobject
25import cairo
26from math import pi, sin, cos, atan2, log, sqrt, hypot, log10
27from colorsys import hls_to_rgb, rgb_to_hls
28
29def map_coords_linear(x,y):
30    return x,1.0-y
31
32def map_coords_spheric(x,y):
33    nx = cos(x * 2 * pi) * y
34    ny = -sin(x * 2 * pi) * y
35    return nx, ny
36
37def get_peaks(f, tolerance=0.01, maxd=0.01, mapfunc=map_coords_linear):
38    corners = 360
39    yc = 1.0/corners
40    peaks = []
41    x0,y0 = 0.0,0.0
42    t0 = -9999.0
43    i0 = 0
44    for i in xrange(int(corners)):
45        p = i*yc
46        a = f(p)
47        x,y = mapfunc(p, a)
48        if i == 0:
49            x0,y0 = x,y
50        t = atan2((y0 - y), (x0 - x)) / (2*pi)
51        td = t - t0
52        if (abs(td) >= tolerance):
53            t0 = t
54            peaks.append((x,y))
55        x0,y0 = x,y
56    return peaks
57
58def make_knobshape(gaps, gapdepth):
59    def knobshape_func(x):
60        x = (x*gaps)%1.0
61        w = 0.5
62        g1 = 0.5 - w*0.5
63        g2 = 0.5 + w*0.5
64        if (x >= g1) and (x < 0.5):
65            x = (x-g1)/(w*0.5)
66            return 0.5 - gapdepth * x * 0.9
67        elif (x >= 0.5) and (x < g2):
68            x = (x-0.5)/(w*0.5)
69            return 0.5 - gapdepth * (1-x) * 0.9
70        else:
71            return 0.5
72    return get_peaks(knobshape_func, 0.03, 0.05, map_coords_spheric)
73
74def hls_to_color(h,l,s):
75    r,g,b = hls_to_rgb(h,l,s)
76    return gtk.gdk.color_parse('#%04X%04X%04X' % (int(r*65535),int(g*65535),int(b*65535)))
77
78def color_to_hls(color):
79    string = color.to_string()
80    r = int(string[1:5], 16) / 65535.0
81    g = int(string[5:9], 16) / 65535.0
82    b = int(string[9:13], 16) / 65535.0
83    return rgb_to_hls(r, g, b)
84
85MARKER_NONE = ''
86MARKER_LINE = 'line'
87MARKER_ARROW = 'arrow'
88MARKER_DOT = 'dot'
89
90LEGEND_NONE = ''
91LEGEND_DOTS = 'dots' # painted dots
92LEGEND_LINES = 'lines' # painted ray-like lines
93LEGEND_RULER = 'ruler' # painted ray-like lines + a circular one
94LEGEND_RULER_INWARDS = 'ruler-inwards' # same as ruler, but the circle is on the outside
95LEGEND_LED_SCALE = 'led-scale' # an LCD scale
96LEGEND_LED_DOTS = 'led-dots' # leds around the knob
97
98class KnobTooltip:
99    def __init__(self):
100        self.tooltip_window = gtk.Window(gtk.WINDOW_POPUP)
101        self.tooltip = gtk.Label()
102        #self.tooltip.modify_fg(gtk.STATE_NORMAL, hls_to_color(0.0, 1.0, 0.0))
103        self.tooltip_timeout = None
104        vbox = gtk.VBox()
105        vbox2 = gtk.VBox()
106        vbox2.add(self.tooltip)
107        vbox2.set_border_width(2)
108        vbox.add(vbox2)
109        self.tooltip_window.add(vbox)
110        vbox.connect('expose-event', self.on_tooltip_expose)
111
112    def show_tooltip(self, knob):
113        text = knob.format_value()
114        rc = knob.get_allocation()
115        x,y = knob.window.get_origin()
116        self.tooltip_window.show_all()
117        w,h = self.tooltip_window.get_size()
118        wx,wy = x+rc.x-w, y+rc.y+rc.height/2-h/2
119        self.tooltip_window.move(wx,wy)
120        rc = self.tooltip_window.get_allocation()
121        self.tooltip_window.window.invalidate_rect((0,0,rc.width,rc.height), False)
122        self.tooltip.set_text(text)
123        if self.tooltip_timeout:
124            gobject.source_remove(self.tooltip_timeout)
125        self.tooltip_timeout = gobject.timeout_add(500, self.hide_tooltip)
126
127    def hide_tooltip(self):
128        self.tooltip_window.hide_all()
129
130    def on_tooltip_expose(self, widget, event):
131        ctx = widget.window.cairo_create()
132        rc = widget.get_allocation()
133        #ctx.set_source_rgb(*hls_to_rgb(0.0, 0.0, 0.5))
134        #ctx.paint()
135        ctx.set_source_rgb(*hls_to_rgb(0.0, 0.0, 0.5))
136        ctx.translate(0.5, 0.5)
137        ctx.set_line_width(1)
138        ctx.rectangle(rc.x, rc.y,rc.width-1,rc.height-1)
139        ctx.stroke()
140        return False
141
142
143
144knob_tooltip = None
145def get_knob_tooltip():
146    global knob_tooltip
147    if not knob_tooltip:
148        knob_tooltip = KnobTooltip()
149    return knob_tooltip
150
151class SmartAdjustment(gtk.Adjustment):
152    def __init__(self, log=False, value=0, lower=0, upper=0, step_incr=0, page_incr=0, page_size=0):
153        self.log = log
154        gtk.Adjustment.__init__(self, value, lower, upper, step_incr, page_incr, page_size)
155        self.normalized_value = self.real2norm(self.value)
156
157    def real2norm(self, value):
158        if self.log:
159            return log(value / self.lower, self.upper / self.lower)
160        else:
161            return (value - self.lower) / (self.upper - self.lower)
162
163    def norm2real(self, value):
164        if self.log:
165            return self.lower * pow(self.upper / self.lower, value)
166        else:
167            return value * (self.upper - self.lower) + self.lower
168
169    def set_value(self, value):
170        self.normalized_value = self.real2norm(value)
171        gtk.Adjustment.set_value(self, value)
172
173    def get_normalized_value(self):
174        return self.normalized_value
175
176    def set_normalized_value(self, value):
177        self.normalized_value = value
178
179        if self.normalized_value < 0.0:
180            self.normalized_value = 0.0
181        elif self.normalized_value > 1.0:
182            self.normalized_value = 1.0
183
184        self.set_value(self.norm2real(self.normalized_value))
185
186class Knob(gtk.VBox):
187    def __init__(self):
188        gtk.VBox.__init__(self)
189        self.gapdepth = 4
190        self.gaps = 10
191        self.value = 0.0
192        self.min_value = 0.0
193        self.max_value = 127.0
194        self.fg_hls = 0.0, 0.7, 0.0
195        self.legend_hls = None
196        self.dragging = False
197        self.start = 0.0
198        self.digits = 2
199        self.segments = 13
200        self.label = ''
201        self.marker = MARKER_LINE
202        self.angle = (3.0/4.0) * 2 * pi
203        self.knobshape = None
204        self.legend = LEGEND_DOTS
205        self.lsize = 2
206        self.lscale = False
207        self.set_double_buffered(True)
208        self.connect('realize', self.on_realize)
209        self.connect("size_allocate", self.on_size_allocate)
210        self.connect('expose-event', self.on_expose)
211        self.set_border_width(6)
212        self.set_size_request(50, 50)
213        self.tooltip_enabled = False
214        self.adj = None
215
216    def set_adjustment(self, adj):
217        self.min_value = 0.0
218        self.max_value = 1.0
219        self.value = adj.get_normalized_value()
220        if self.adj:
221            self.adj.disconnect(self.adj_id)
222        self.adj = adj
223        self.adj_id = adj.connect("value-changed", self.on_adj_value_changed)
224
225    def is_sensitive(self):
226        return self.get_property("sensitive")
227
228    def format_value(self):
229        if self.adj:
230            value = self.adj.value
231        else:
232            value = self.value
233        return ("%%.%if" % self.digits) % value
234
235    def show_tooltip(self):
236        if self.tooltip_enabled:
237            get_knob_tooltip().show_tooltip(self)
238
239    def on_realize(self, widget):
240        self.root = self.get_toplevel()
241        self.root.add_events(gtk.gdk.ALL_EVENTS_MASK)
242        self.root.connect('scroll-event', self.on_mousewheel)
243        self.root.connect('button-press-event', self.on_left_down)
244        self.root.connect('button-release-event', self.on_left_up)
245        self.root.connect('motion-notify-event', self.on_motion)
246        self.update_knobshape()
247
248    def update_knobshape(self):
249        rc = self.get_allocation()
250        b = self.get_border_width()
251        size = min(rc.width, rc.height) - 2*b
252        gd = float(self.gapdepth*0.5) / size
253        self.gd = gd
254        self.knobshape = make_knobshape(self.gaps, gd)
255
256    def set_legend_scale(self, scale):
257        self.lscale = scale
258        self.refresh()
259
260    def set_legend_line_width(self, width):
261        self.lsize = width
262        self.refresh()
263
264    def set_segments(self, segments):
265        self.segments = segments
266        self.refresh()
267
268    def set_marker(self, marker):
269        self.marker = marker
270        self.refresh()
271
272    def set_range(self, minvalue, maxvalue):
273        self.min_value = minvalue
274        self.max_value = maxvalue
275        self.set_value(self.value)
276
277    def quantize_value(self, value):
278        scaler = 10**self.digits
279        value = int((value*scaler)+0.5) / float(scaler)
280        return value
281
282    def on_adj_value_changed(self, adj):
283        new_value = adj.get_normalized_value()
284        if self.value != new_value:
285            self.value = new_value
286            self.refresh()
287
288    def set_value(self, value):
289        oldval = self.value
290        self.value = min(max(self.quantize_value(value), self.min_value), self.max_value)
291        if self.value != oldval:
292            if self.adj:
293                self.adj.set_normalized_value(value)
294            self.refresh()
295
296    def get_value(self):
297        return self.value
298
299    def set_top_color(self, h, l, s):
300        self.fg_hls = h,l,s
301        self.refresh()
302
303    def set_legend_color(self, h, l, s):
304        self.legend_hls = h,l,s
305        self.refresh()
306
307    def get_top_color(self):
308        return self.fg_hls
309
310    def set_gaps(self, gaps):
311        self.gaps = gaps
312        self.knobshape = None
313        self.refresh()
314
315    def get_gaps(self):
316        return self.gaps
317
318    def set_gap_depth(self, gapdepth):
319        self.gapdepth = gapdepth
320        self.knobshape = None
321        self.refresh()
322
323    def get_gap_depth(self):
324        return self.gapdepth
325
326    def set_angle(self, angle):
327        self.angle = angle
328        self.refresh()
329
330    def get_angle(self):
331        return self.angle
332
333    def set_legend(self, legend):
334        self.legend = legend
335        self.refresh()
336
337    def get_legend(self):
338        return self.legend
339
340    def on_left_down(self, widget, event):
341        #print "on_left_down"
342
343        # dont drag insensitive widgets
344        if not self.is_sensitive():
345            return False
346
347        if not sum(self.get_allocation().intersect((int(event.x), int(event.y), 1, 1))):
348            return False
349        if event.button == 1:
350            #print "start draggin"
351            self.startvalue = self.value
352            self.start = event.y
353            self.dragging = True
354            self.show_tooltip()
355            self.grab_add()
356            return True
357        return False
358
359    def on_left_up(self, widget, event):
360        #print "on_left_up"
361        if not self.dragging:
362            return False
363        if event.button == 1:
364            #print "stop draggin"
365            self.dragging = False
366            self.grab_remove()
367            return True
368        return False
369
370    def on_motion(self, widget, event):
371        #print "on_motion"
372
373        # dont drag insensitive widgets
374        if not self.is_sensitive():
375            return False
376
377        if self.dragging:
378            x,y,state = self.window.get_pointer()
379            rc = self.get_allocation()
380            range = self.max_value - self.min_value
381            scale = rc.height
382            if event.state & gtk.gdk.SHIFT_MASK:
383                scale = rc.height*8
384            value = self.startvalue - ((y - self.start)*range)/scale
385            oldval = self.value
386            self.set_value(value)
387            self.show_tooltip()
388            if oldval != self.value:
389                self.start = y
390                self.startvalue = self.value
391            return True
392        return False
393
394    def on_mousewheel(self, widget, event):
395
396        # dont move insensitive widgets
397        if not self.is_sensitive():
398            return False
399
400        if not sum(self.get_allocation().intersect((int(event.x), int(event.y), 1, 1))):
401            return
402        range = self.max_value - self.min_value
403        minstep = 1.0 / (10**self.digits)
404        if event.state & (gtk.gdk.SHIFT_MASK | gtk.gdk.BUTTON1_MASK):
405            step = minstep
406        else:
407            step = max(self.quantize_value(range/25.0), minstep)
408        value = self.value
409        if event.direction == gtk.gdk.SCROLL_UP:
410            value += step
411        elif event.direction == gtk.gdk.SCROLL_DOWN:
412            value -= step
413        self.set_value(value)
414        self.show_tooltip()
415
416    def on_size_allocate(self, widget, allocation):
417        #print allocation.x, allocation.y, allocation.width, allocation.height
418        self.update_knobshape()
419
420    def draw_points(self, ctx, peaks):
421        ctx.move_to(*peaks[0])
422        for peak in peaks[1:]:
423            ctx.line_to(*peak)
424
425    def draw(self, ctx):
426        if not self.legend_hls:
427            self.legend_hls = color_to_hls(self.style.fg[gtk.STATE_NORMAL])
428
429        if not self.knobshape:
430            self.update_knobshape()
431        startangle = pi*1.5 - self.angle*0.5
432        angle = ((self.value - self.min_value) / (self.max_value - self.min_value)) * self.angle + startangle
433        rc = self.get_allocation()
434        size = min(rc.width, rc.height)
435
436        kh = self.get_border_width() # knob height
437
438        ps = 1.0/size # pixel size
439        ps2 = 1.0 / (size-(2*kh)-1) # pixel size inside knob
440        ss = ps * kh # shadow size
441        lsize = ps2 * self.lsize # legend line width
442        # draw spherical
443        ctx.translate(rc.x, rc.y)
444        ctx.translate(0.5,0.5)
445        ctx.translate(size*0.5, size*0.5)
446        ctx.scale(size-(2*kh)-1, size-(2*kh)-1)
447        if self.legend == LEGEND_DOTS:
448            ctx.save()
449            ctx.set_source_rgb(*hls_to_rgb(*self.legend_hls))
450            dots = self.segments
451            for i in xrange(dots):
452                s = float(i)/(dots-1)
453                a = startangle + self.angle*s
454                ctx.save()
455                ctx.rotate(a)
456                r = lsize*0.5
457                if self.lscale:
458                    r = max(r*s,ps2)
459                ctx.arc(0.5+lsize, 0.0, r, 0.0, 2*pi)
460                ctx.fill()
461                ctx.restore()
462            ctx.restore()
463        elif self.legend in (LEGEND_LINES, LEGEND_RULER, LEGEND_RULER_INWARDS):
464            ctx.save()
465            ctx.set_source_rgb(*hls_to_rgb(*self.legend_hls))
466            dots = self.segments
467            n = ps2*(kh-1)
468            for i in xrange(dots):
469                s = float(i)/(dots-1)
470                a = startangle + self.angle*s
471                ctx.save()
472                ctx.rotate(a)
473                r = n*0.9
474                if self.lscale:
475                    r = max(r*s,ps2)
476                ctx.move_to(0.5+ps2+n*0.1, 0.0)
477                ctx.line_to(0.5+ps2+n*0.1+r, 0.0)
478                ctx.set_line_width(lsize)
479                ctx.stroke()
480                ctx.restore()
481            ctx.restore()
482            if self.legend == LEGEND_RULER:
483                ctx.save()
484                ctx.set_source_rgb(*hls_to_rgb(*self.legend_hls))
485                ctx.set_line_width(lsize)
486                ctx.arc(0.0, 0.0, 0.5+ps2+n*0.1, startangle, startangle+self.angle)
487                ctx.stroke()
488                ctx.restore()
489            elif self.legend == LEGEND_RULER_INWARDS:
490                ctx.save()
491                ctx.set_source_rgb(*hls_to_rgb(*self.legend_hls))
492                ctx.set_line_width(lsize)
493                ctx.arc(0.0, 0.0, 0.5+ps2+n, startangle, startangle+self.angle)
494                ctx.stroke()
495
496        # draw shadow only for sensitive widgets that have height
497        if self.is_sensitive() and kh:
498            ctx.save()
499            ctx.translate(ss, ss)
500            ctx.rotate(angle)
501            self.draw_points(ctx, self.knobshape)
502            ctx.close_path()
503            ctx.restore()
504            ctx.set_source_rgba(0,0,0,0.3)
505            ctx.fill()
506
507        if self.legend in (LEGEND_LED_SCALE, LEGEND_LED_DOTS):
508            ch,cl,cs = self.legend_hls
509            n = ps2*(kh-1)
510            ctx.save()
511            ctx.set_line_cap(cairo.LINE_CAP_ROUND)
512            ctx.set_source_rgb(*hls_to_rgb(ch,cl*0.2,cs))
513            ctx.set_line_width(lsize)
514            ctx.arc(0.0, 0.0, 0.5+ps2+n*0.5, startangle, startangle+self.angle)
515            ctx.stroke()
516            ctx.set_source_rgb(*hls_to_rgb(ch,cl,cs))
517            if self.legend == LEGEND_LED_SCALE:
518                ctx.set_line_width(lsize-ps2*2)
519                ctx.arc(0.0, 0.0, 0.5+ps2+n*0.5, startangle, angle)
520                ctx.stroke()
521            elif self.legend == LEGEND_LED_DOTS:
522                dots = self.segments
523                dsize = lsize-ps2*2
524                seg = self.angle/dots
525                endangle = startangle + self.angle
526                for i in xrange(dots):
527                    s = float(i)/(dots-1)
528                    a = startangle + self.angle*s
529                    if ((a-seg*0.5) > angle) or (angle == startangle):
530                        break
531                    ctx.save()
532                    ctx.rotate(a)
533                    r = dsize*0.5
534                    if self.lscale:
535                        r = max(r*s,ps2)
536                    ctx.arc(0.5+ps2+n*0.5, 0.0, r, 0.0, 2*pi)
537                    ctx.fill()
538                    ctx.restore()
539            ctx.restore()
540        pat = cairo.LinearGradient(-0.5, -0.5, 0.5, 0.5)
541        pat.add_color_stop_rgb(1.0, 0.2,0.2,0.2)
542        pat.add_color_stop_rgb(0.0, 0.3,0.3,0.3)
543        ctx.set_source(pat)
544        ctx.rotate(angle)
545        self.draw_points(ctx, self.knobshape)
546        ctx.close_path()
547        ctx.fill_preserve()
548        ctx.set_source_rgba(0.1,0.1,0.1,1)
549        ctx.save()
550        ctx.identity_matrix()
551        ctx.set_line_width(1.0)
552        ctx.stroke()
553        ctx.restore()
554
555        ctx.arc(0.0, 0.0, 0.5-self.gd, 0.0, pi*2.0)
556        ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], max(self.fg_hls[1]*0.4,0.0), self.fg_hls[2]))
557        ctx.fill()
558        ctx.arc(0.0, 0.0, 0.5-self.gd-ps, 0.0, pi*2.0)
559        ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], min(self.fg_hls[1]*1.2,1.0), self.fg_hls[2]))
560        ctx.fill()
561        ctx.arc(0.0, 0.0, 0.5-self.gd-(2*ps), 0.0, pi*2.0)
562        ctx.set_source_rgb(*hls_to_rgb(*self.fg_hls))
563        ctx.fill()
564
565        # dont draw cap for insensitive widgets
566        if not self.is_sensitive():
567            return
568
569        #~ ctx.set_line_cap(cairo.LINE_CAP_ROUND)
570        #~ ctx.move_to(0.5-0.3-self.gd-ps, 0.0)
571        #~ ctx.line_to(0.5-self.gd-ps*5, 0.0)
572
573        if self.marker == MARKER_LINE:
574            ctx.set_line_cap(cairo.LINE_CAP_BUTT)
575            ctx.move_to(0.5-0.3-self.gd-ps, 0.0)
576            ctx.line_to(0.5-self.gd-ps, 0.0)
577            ctx.save()
578            ctx.identity_matrix()
579            ctx.translate(0.5,0.5)
580            ctx.set_line_width(5)
581            ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], min(self.fg_hls[1]*1.2,1.0), self.fg_hls[2]))
582            ctx.stroke_preserve()
583            ctx.set_line_width(3)
584            ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], max(self.fg_hls[1]*0.4,0.0), self.fg_hls[2]))
585            ctx.stroke()
586            ctx.restore()
587        elif self.marker == MARKER_DOT:
588            ctx.arc(0.5-0.05-self.gd-ps*5, 0.0, 0.05, 0.0, 2*pi)
589            ctx.save()
590            ctx.identity_matrix()
591            ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], min(self.fg_hls[1]*1.2,1.0), self.fg_hls[2]))
592            ctx.stroke_preserve()
593            ctx.set_line_width(1)
594            ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], max(self.fg_hls[1]*0.4,0.0), self.fg_hls[2]))
595            ctx.fill()
596            ctx.restore()
597        elif self.marker == MARKER_ARROW:
598            ctx.set_line_cap(cairo.LINE_CAP_BUTT)
599            ctx.move_to(0.5-0.3-self.gd-ps, 0.1)
600            ctx.line_to(0.5-0.1-self.gd-ps, 0.0)
601            ctx.line_to(0.5-0.3-self.gd-ps, -0.1)
602            ctx.close_path()
603            ctx.save()
604            ctx.identity_matrix()
605            #~ ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], min(self.fg_hls[1]*1.2,1.0), self.fg_hls[2]))
606            #~ ctx.stroke_preserve()
607            ctx.set_line_width(1)
608            ctx.set_source_rgb(*hls_to_rgb(self.fg_hls[0], max(self.fg_hls[1]*0.4,0.0), self.fg_hls[2]))
609            ctx.fill()
610            ctx.restore()
611
612    def refresh(self):
613        rect = self.get_allocation()
614        if self.window:
615            self.window.invalidate_rect(rect, False)
616        return True
617
618    def on_expose(self, widget, event):
619        self.context = self.window.cairo_create()
620        self.draw(self.context)
621        return False
622
623class filter_band:
624    def __init__(self):
625        self.fsamp = 48e3
626
627    def set_params(self, freq, bandw, gain):
628        freq_ratio = freq / self.fsamp
629        gain2 = pow(10.0, 0.05 * gain)
630        b = 7 * bandw * freq_ratio / sqrt(gain2)
631        self.gn = 0.5 * (gain2 - 1)
632        self.v1 = -cos(2 * pi * freq_ratio)
633        self.v2 = (1 - b) / (1 + b)
634        self.v1 *= (1 + self.v2)
635        self.gn *= (1 - self.v2)
636
637    def get_response(self, freq):
638        w = 2 * pi * (freq / self.fsamp)
639        c1 = cos(w)
640        s1 = sin(w)
641        c2 = cos(2 * w)
642        s2 = sin(2 * w)
643
644        x = c2 + self.v1 * c1 + self.v2
645        y = s2 + self.v1 * s1
646        t1 = hypot(x, y)
647        x += self.gn * (c2 - 1)
648        y += self.gn * s2
649        t2 = hypot(x, y)
650
651        #return t2 / t1
652        return 20 * log10(t2 / t1)
653
654class frequency_response(gtk.DrawingArea):
655    def __init__(self):
656        gtk.DrawingArea.__init__(self)
657
658        self.connect("expose-event", self.on_expose)
659        self.connect("size-request", self.on_size_request)
660        self.connect("size_allocate", self.on_size_allocate)
661
662        self.color_bg = gtk.gdk.Color(0,0,0)
663        self.color_value = gtk.gdk.Color(int(65535 * 0.8), int(65535 * 0.7), 0)
664        self.color_mark = gtk.gdk.Color(int(65535 * 0.3), int(65535 * 0.3), int(65535 * 0.3))
665        self.color_sum = gtk.gdk.Color(int(65535 * 1.0), int(65535 * 1.0), int(65535 * 1.0))
666        self.width = 0
667        self.height = 0
668        self.margin = 10
669        self.db_range = 30
670        self.master_gain = 0.0
671        self.master_enabled = False
672
673        self.filters = {}
674
675    def on_expose(self, widget, event):
676        cairo_ctx = widget.window.cairo_create()
677
678        # set a clip region for the expose event
679        cairo_ctx.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
680        cairo_ctx.clip()
681
682        self.draw(cairo_ctx)
683
684        return False
685
686    def on_size_allocate(self, widget, allocation):
687        #print allocation.x, allocation.y, allocation.width, allocation.height
688        self.width = float(allocation.width)
689        self.height = float(allocation.height)
690        self.font_size = 10
691
692    def on_size_request(self, widget, requisition):
693        #print "size-request, %u x %u" % (requisition.width, requisition.height)
694        requisition.width = 150
695        requisition.height = 150
696        return
697
698    def invalidate_all(self):
699        self.queue_draw_area(0, 0, int(self.width), int(self.height))
700
701    def get_x(self, hz):
702        width = self.width - 3.5 * self.margin
703        #x = self.margin + width * (hz - 20) / (20000 - 20)
704        x = 2.5 * self.margin + width * log(hz / 20.0, 1000.0)
705        #print x
706        return x
707
708    def get_freq(self, x):
709        width = self.width - 3.5 * self.margin
710        return 20 * pow(1000, (x - 2.5 * self.margin) / width)
711
712    def get_y(self, db):
713        height = self.height - 2.5 * self.margin
714        y = self.margin + height * (self.db_range - db) / (self.db_range * 2)
715        #print y
716        return y
717
718    def draw_db_grid(self, cairo_ctx, db):
719        x = self.get_x(20)
720        y = self.get_y(db)
721        cairo_ctx.move_to(x, y)
722        cairo_ctx.line_to(self.get_x(20000), y)
723
724        if db % 10 == 0:
725            x -= 20
726            y += 3
727            cairo_ctx.move_to(x, y)
728            label = "%+d" % db
729            if db == 0:
730                label = " " + label
731            cairo_ctx.show_text(label)
732
733        cairo_ctx.stroke()
734
735    def invalidate_all(self):
736        self.queue_draw_area(0, 0, int(self.width), int(self.height))
737
738    def draw(self, cairo_ctx):
739        cairo_ctx.select_font_face("Fixed")
740
741        cairo_ctx.set_source_color(self.color_bg)
742        cairo_ctx.rectangle(0, 0, self.width, self.height)
743        cairo_ctx.fill()
744
745        cairo_ctx.set_source_color(self.color_mark)
746        cairo_ctx.set_line_width(1);
747
748        for hz in range(20, 101, 10) + range(100, 1001, 100) + range(1000, 10001, 1000) + range(10000, 20001, 10000):
749            if hz >= 10000:
750                label = "%dk" % int(hz / 1000)
751            elif hz >= 1000:
752                label = "%dk" % int(hz / 1000)
753            else:
754                label = "%d" % int(hz)
755            first_digit = int(label[0])
756            if first_digit > 5 or (first_digit > 3 and (len(label) == 3)):
757                label = None
758
759            x = self.get_x(hz)
760            cairo_ctx.move_to(x, self.get_y(self.db_range))
761            y = self.get_y(-self.db_range)
762            cairo_ctx.line_to(x, y)
763            if label:
764                y += 10
765                if hz == 20000:
766                    x -= 15
767                elif hz != 20:
768                    x -= 3
769                cairo_ctx.move_to(x, y)
770                cairo_ctx.show_text(label)
771            cairo_ctx.stroke()
772
773        for db in range(0, self.db_range + 1, 5):
774            self.draw_db_grid(cairo_ctx, db)
775
776            if db != 0:
777                self.draw_db_grid(cairo_ctx, -db)
778
779        curves = [[x, {}, self.master_gain, self.get_freq(x)] for x in range(int(self.get_x(20)), int(self.get_x(20e3)))]
780        #print repr(curves)
781
782        # calculate filter responses
783        for label, filter in self.filters.items():
784            if not filter.enabled:
785                continue
786
787            for point in curves:
788                db = filter.get_response(point[3])
789                point[1][label] = [self.get_y(db), db]
790
791        # calculate sum curve
792        for point in curves:
793            for label, filter_point in point[1].items():
794                point[2] += filter_point[1]
795            #print point
796
797        # draw filter curves
798        for label, filter in self.filters.items():
799            if not filter.enabled:
800                continue
801
802            cairo_ctx.set_source_color(filter.color)
803            cairo_ctx.move_to(curves[0][0], curves[0][1][label][0])
804            for point in curves:
805                cairo_ctx.line_to(point[0], point[1][label][0])
806            cairo_ctx.stroke()
807
808        if self.master_enabled:
809            # draw sum curve
810            cairo_ctx.set_source_color(self.color_sum)
811            cairo_ctx.set_line_width(2);
812            cairo_ctx.move_to(curves[0][0], self.get_y(curves[0][2]))
813            for point in curves:
814                cairo_ctx.line_to(point[0], self.get_y(point[2]))
815            cairo_ctx.stroke()
816
817        # draw base point markers
818        for label, filter in self.filters.items():
819            if not filter.enabled:
820                continue
821
822            cairo_ctx.set_source_color(self.color_value)
823            x = self.get_x(filter.adj_hz.value)
824            y = self.get_y(filter.adj_db.value)
825
826            cairo_ctx.move_to(x, y)
827            cairo_ctx.show_text(label)
828            cairo_ctx.stroke()
829
830    def add_filter(self, label, adj_hz, adj_db, adj_bw, color):
831        #print "filter %s added (%.2f Hz, %.2f dB, %.2f bw)" % (label, adj_hz.value, adj_db.value, adj_bw.value)
832        filter = filter_band()
833        filter.enabled = False
834        filter.label = label
835        filter.color = color
836        filter.set_params(adj_hz.value, adj_bw.value, adj_db.value)
837        adj_hz.filter = filter
838        adj_db.filter = filter
839        adj_bw.filter = filter
840        filter.adj_hz = adj_hz
841        filter.adj_db = adj_db
842        filter.adj_bw = adj_bw
843        adj_hz.connect("value-changed", self.on_value_change_request)
844        adj_db.connect("value-changed", self.on_value_change_request)
845        adj_bw.connect("value-changed", self.on_value_change_request)
846        self.filters[label] = filter
847
848    def enable_filter(self, label):
849        filter = self.filters[label]
850        #print "filter %s enabled (%.2f Hz, %.2f dB, %.2f bw)" % (label, filter.adj_hz.value, filter.adj_db.value, filter.adj_bw.value)
851        filter.enabled = True
852        self.invalidate_all()
853
854    def disable_filter(self, label):
855        filter = self.filters[label]
856        #print "filter %s disabled (%.2f Hz, %.2f dB, %.2f bw)" % (label, filter.adj_hz.value, filter.adj_db.value, filter.adj_bw.value)
857        filter.enabled = False
858        self.invalidate_all()
859
860    def on_value_change_request(self, adj):
861        #print "adj changed"
862        adj.filter.set_params(adj.filter.adj_hz.value, adj.filter.adj_bw.value, adj.filter.adj_db.value)
863        self.invalidate_all()
864
865    def master_enable(self):
866        self.master_enabled = True;
867        self.invalidate_all()
868
869    def master_disable(self):
870        self.master_enabled = False;
871        self.invalidate_all()
872
873    def set_master_gain(self, gain):
874        self.master_gain = gain;
875        self.invalidate_all()
876
877class filter_ui:
878    def __init__(self, argv):
879        self.fake = len(argv) == 1
880
881        if self.fake:
882            self.plugin_uri = self.human_id = "fake"
883            self.bundle_path = "."
884            self.shown = False
885        else:
886            #print repr(argv)
887            self.plugin_uri = argv[1]
888            self.bundle_path = argv[2]
889            self.human_id = argv[3]
890
891            self.recv_pipe_fd = int(argv[4])
892            self.send_pipe_fd = int(argv[5])
893
894            oldflags = fcntl.fcntl(self.recv_pipe_fd, fcntl.F_GETFL)
895            fcntl.fcntl(self.recv_pipe_fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
896
897            self.recv_pipe = os.fdopen(self.recv_pipe_fd, 'r')
898            self.send_pipe = os.fdopen(self.send_pipe_fd, 'w')
899
900        if self.plugin_uri == "http://nedko.aranaudov.org/soft/filter/2/mono":
901            self.port_base = 2
902        elif self.plugin_uri == "http://nedko.aranaudov.org/soft/filter/2/stereo":
903            self.port_base = 4
904        elif self.plugin_uri == "fake":
905            self.port_base = 0
906        else:
907            return
908
909        self.lv2logo = gtk.gdk.pixbuf_new_from_file(self.bundle_path + "/lv2logo.png")
910
911        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
912        #self.window.set_size_request(600, 400)
913        self.window.set_title("%s (4-band parametric filter)" % self.human_id)
914        self.window.set_role("plugin_ui")
915
916        self.top_vbox = gtk.VBox()
917        self.top_vbox.set_spacing(10)
918
919        align = gtk.Alignment(0.5, 0.5, 1.0, 1.0)
920        align.set_padding(10, 10, 10, 10)
921        align.add(self.top_vbox)
922
923        self.window.add(align)
924
925        self.fr = frequency_response()
926        self.fr.set_size_request(400, 200)
927
928        frame = gtk.Frame()
929        frame.set_shadow_type(gtk.SHADOW_ETCHED_OUT)
930        frame.add(self.fr)
931
932        self.top_vbox.pack_start(frame, True, True)
933
934        self.param_hbox = gtk.HBox()
935        self.top_vbox.pack_start(self.param_hbox)
936
937        self.param_hbox.set_spacing(10)
938
939        self.initator = False
940
941        self.ports = []
942
943        misc_box = gtk.VBox()
944        misc_box.set_spacing(5)
945
946        master_frame = gtk.Frame("Master")
947        master_frame.set_label_align(0.5, 0.5)
948
949        master_box = gtk.VBox()
950        master_box.set_spacing(5)
951
952        port = {'index': 0, 'name': 'Active', 'type': 'toggle'}
953        self.ports.append(port)
954        self.add_param_box(master_box, self.create_toggle_box(port))
955
956        port = {'index': 1, 'name': 'Gain', 'type': 'knob', 'min': -20.0, 'max': 20.0, 'unit': 'dB', 'log': False}
957        self.ports.append(port)
958        self.add_param_box(master_box, self.create_knob_box(port))
959
960        master_frame.add(master_box)
961        misc_box.pack_start(master_frame, False, False)
962
963        #logo = gtk.Image()
964        #logo.set_from_pixbuf(self.lv2logo)
965        #misc_box.pack_start(logo, True, True)
966
967        button_box = gtk.VBox()
968
969        button = gtk.Button(stock=gtk.STOCK_ABOUT)
970        button.connect("clicked", self.on_about)
971        button_box.pack_start(button)
972
973        button = gtk.Button(stock=gtk.STOCK_CLOSE)
974        button.connect("clicked", self.on_window_closed)
975        button_box.pack_start(button)
976
977        align = gtk.Alignment(0.5, 1.0, 1.0, 0.0)
978        align.add(button_box)
979        misc_box.pack_start(align, True, True)
980
981        band_parameters = [
982            {'name': 'Active', 'type': 'toggle'},
983            {'name': 'Frequency', 'type': 'knob', 'unit': 'Hz', 'log': True},
984            {'name': 'Bandwidth', 'type': 'knob', 'min': 0.125, 'max': 8.0, 'unit': '', 'log': True},
985            {'name': 'Gain', 'type': 'knob', 'min': -20.0, 'max': 20.0, 'unit': 'dB', 'log': False}]
986
987        freq_min = [  20.0,   40.0,   100.0,   200.0]
988        freq_max = [2000.0, 4000.0, 10000.0, 20000.0]
989
990        port_index = 2
991
992        filter_colors = [gtk.gdk.Color(int(65535 * 1.0), int(65535 * 0.6), int(65535 * 0.0)),
993                         gtk.gdk.Color(int(65535 * 0.6), int(65535 * 1.0), int(65535 * 0.6)),
994                         gtk.gdk.Color(int(65535 * 0.0), int(65535 * 0.6), int(65535 * 1.0)),
995                         gtk.gdk.Color(int(65535 * 0.9), int(65535 * 0.0), int(65535 * 0.5))]
996
997        for i in [0, 1, 2, 3]:
998            band_frame = gtk.Frame("Band %d" % (i + 1))
999            band_frame.set_label_align(0.5, 0.5)
1000
1001            band_box = gtk.VBox()
1002            band_box.set_spacing(5)
1003
1004            for parameter in band_parameters:
1005
1006                port = parameter.copy()
1007                port['index'] = port_index
1008                port_index += 1
1009
1010                if port['name'] == 'Frequency':
1011                    port['min'] = freq_min[i]
1012                    port['max'] = freq_max[i]
1013
1014                self.ports.append(port)
1015
1016                #param_box.set_spacing(5)
1017                if port['type'] == 'knob':
1018                    self.add_param_box(band_box, self.create_knob_box(port))
1019                elif port['type'] == 'toggle':
1020                    self.add_param_box(band_box, self.create_toggle_box(port))
1021
1022            self.fr.add_filter(
1023                str(i + 1),
1024                self.ports[port_index - 3]['adj'], # frequency
1025                self.ports[port_index - 1]['adj'], # gain
1026                self.ports[port_index - 2]['adj'], # bandwidth
1027                filter_colors[i])
1028
1029            band_frame.add(band_box)
1030
1031            self.param_hbox.pack_start(band_frame, True, True)
1032
1033        self.param_hbox.pack_start(misc_box, True, True)
1034
1035        self.initator = True
1036
1037    def on_about(self, widget):
1038        about = gtk.AboutDialog()
1039        about.set_transient_for(self.window)
1040        about.set_name("4-band parametric filter")
1041        #about.set_website(program_data['website'])
1042        about.set_authors(["Nedko Arnaudov - LV2 plugin and GUI", 'Fons Adriaensen - DSP code'])
1043        about.set_artists(["LV2 logo has been designed by Thorsten Wilms, based on a concept from Peter Shorthose."])
1044        about.set_logo(self.lv2logo)
1045        about.show()
1046        about.run()
1047        about.hide()
1048
1049    def create_knob_box(self, port):
1050        param_box = gtk.VBox()
1051        step = (port['max'] - port['min']) / 100
1052        adj = SmartAdjustment(port['log'], port['min'], port['min'], port['max'], step, step * 20)
1053        adj.port = port
1054        port['adj'] = adj
1055
1056        adj.connect("value-changed", self.on_adj_value_changed)
1057
1058        knob = Knob()
1059        knob.set_adjustment(adj)
1060        align = gtk.Alignment(0.5, 0.5, 0, 0)
1061        align.set_padding(0, 0, 20, 20)
1062        align.add(knob)
1063        param_box.pack_start(align, False)
1064
1065        adj.label = gtk.Label(self.get_adj_value_text(adj)[0])
1066        param_box.pack_start(adj.label, False)
1067        #spin = gtk.SpinButton(adj, 0.0, 2)
1068        #param_box.pack_start(spin, False)
1069
1070        label = gtk.Label(port['name'])
1071        param_box.pack_start(label, False)
1072        return param_box
1073
1074    def create_toggle_box(self, port):
1075        param_box = gtk.VBox()
1076        button = gtk.CheckButton(port['name'])
1077        button.port = port
1078        port['widget'] = button
1079
1080        button.connect("toggled", self.on_button_toggled)
1081
1082        align = gtk.Alignment(0.5, 0.5, 0, 0)
1083        align.add(button)
1084        param_box.pack_start(align, False)
1085        return param_box
1086
1087    def add_param_box(self, container, param_box):
1088        align = gtk.Alignment(0.5, 0.5, 1.0, 1.0)
1089        align.set_padding(10, 10, 10, 10)
1090        align.add(param_box)
1091
1092        container.pack_start(align, True)
1093
1094    def get_adj_value_text(self, adj):
1095        value = adj.get_value()
1096        if value >= 10000:
1097            format = "%.0f"
1098        elif value >= 1000:
1099            format = "%.1f"
1100        else:
1101            format = "%.2f"
1102        text = format % value
1103        unit = adj.port['unit']
1104        if unit:
1105            text += " " + unit
1106
1107        return value, text
1108
1109    def on_adj_value_changed(self, adj):
1110        value, text = self.get_adj_value_text(adj)
1111        adj.label.set_text(text)
1112
1113        if adj.port['index'] == 1:
1114            #print "Master gain = %.2f dB" % adj.get_value()
1115            self.fr.set_master_gain(adj.get_value())
1116
1117        if self.initator:
1118            #print adj.port, adj.get_value()
1119            self.send_port_value(adj.port['index'] + self.port_base, value)
1120
1121    def on_button_toggled(self, widget):
1122        port_index = widget.port['index']
1123        band_no = (port_index - 2) / 4 + 1
1124        if widget.get_active():
1125            value = 1.0
1126            if band_no > 0:
1127                self.fr.enable_filter(str(band_no))
1128            else:
1129                self.fr.master_enable()
1130        else:
1131            value = 0.0
1132            if band_no > 0:
1133                self.fr.disable_filter(str(band_no))
1134            else:
1135                self.fr.master_disable()
1136
1137        if self.initator:
1138            self.send_port_value(port_index + self.port_base, value)
1139
1140    def send(self, lines):
1141        if self.fake:
1142            return
1143
1144        for line in lines:
1145            #print 'send: "' + line + '"'
1146            self.send_pipe.write(line + "\n")
1147            self.send_pipe.flush()
1148
1149    def send_exiting(self):
1150        self.send(["exiting"])
1151
1152    def send_port_value(self, port_index, value):
1153        self.send(["port_value", str(port_index), "%.10f" % value])
1154
1155    def send_hi(self):
1156        self.send([""])         # send empty line (just newline char)
1157
1158    def recv_line(self):
1159        return self.recv_pipe.readline().strip()
1160
1161    def recv_command(self):
1162        try:
1163            msg = self.recv_line()
1164
1165            if msg == "port_value":
1166                port_index = int(self.recv_line())
1167                port_value = float(self.recv_line())
1168                #print "port_value_change recevied: %d %f" % (port_index, port_value)
1169                self.on_port_value_changed(port_index, port_value)
1170            elif msg == "show":
1171                self.on_show()
1172            elif msg == "hide":
1173                self.on_hide()
1174            elif msg == "quit":
1175                self.on_quit()
1176            else:
1177                print 'unknown message: "' + msg + '"'
1178
1179            return True
1180        except IOError:
1181            return False
1182
1183    def on_recv(self, fd, cond):
1184        #print "on_recv"
1185        if cond == gobject.IO_HUP:
1186            gtk.main_quit()
1187            return False
1188
1189        while True:
1190            if not self.recv_command():
1191                break
1192
1193        return True
1194
1195    def run(self):
1196        self.window.connect("destroy", self.on_window_closed)
1197
1198        if self.fake:
1199            if not self.shown:
1200                self.shown = True
1201                self.on_show()
1202        else:
1203            self.send_hi()
1204            gobject.io_add_watch(self.recv_pipe_fd, gobject.IO_IN | gobject.IO_HUP, self.on_recv)
1205
1206        gtk.main()
1207
1208    def on_port_value_changed(self, port_index, port_value):
1209        #print "port %d set to %f" % (port_index, port_value)
1210        port_index -= self.port_base
1211        port = self.ports[port_index]
1212        #print repr(port)
1213        port_type = port['type']
1214        if port_type == 'knob':
1215            self.initator = False
1216            port['adj'].set_value(port_value)
1217            self.initator = True
1218        elif port_type == 'toggle':
1219            if port_value > 0.0:
1220                toggled = True
1221            else:
1222                toggled = False
1223
1224            self.initator = False
1225            port['widget'].set_active(toggled)
1226            self.initator = True
1227
1228    def on_show(self):
1229        self.window.show_all()
1230
1231    def on_hide(self):
1232        self.window.hide_all()
1233
1234    def on_quit(self):
1235        gtk.main_quit()
1236
1237    def on_window_closed(self, arg):
1238        self.send_exiting()
1239        gtk.main_quit()
1240
1241def main():
1242    filter_ui(sys.argv).run()
1243    #print "main done"
1244
1245if __name__ == '__main__':
1246    main()
Note: See TracBrowser for help on using the browser.