52b898a5d03bd3b992da8bcc5b3a035d91b5858e
[debian/gnuradio] / gr-wxgui / src / python / fftsink2.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2003,2004,2005,2006,2007 Free Software Foundation, Inc.
4
5 # This file is part of GNU Radio
6
7 # GNU Radio 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; either version 3, or (at your option)
10 # any later version.
11
12 # GNU Radio is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with GNU Radio; see the file COPYING.  If not, write to
19 # the Free Software Foundation, Inc., 51 Franklin Street,
20 # Boston, MA 02110-1301, USA.
21
22
23 from gnuradio import gr, gru, window
24 from gnuradio.wxgui import stdgui2
25 import wx
26 import gnuradio.wxgui.plot as plot
27 import numpy
28 import threading
29 import math    
30
31 default_fftsink_size = (640,240)
32 default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
33
34 class fft_sink_base(object):
35     def __init__(self, input_is_real=False, baseband_freq=0, y_per_div=10, ref_level=50,
36                  sample_rate=1, fft_size=512,
37                  fft_rate=default_fft_rate,
38                  average=False, avg_alpha=None, title='', peak_hold=False):
39
40         # initialize common attributes
41         self.baseband_freq = baseband_freq
42         self.y_divs = 8
43         self.y_per_div=y_per_div
44         self.ref_level = ref_level
45         self.sample_rate = sample_rate
46         self.fft_size = fft_size
47         self.fft_rate = fft_rate
48         self.average = average
49         if avg_alpha is None:
50             self.avg_alpha = 2.0 / fft_rate
51         else:
52             self.avg_alpha = avg_alpha
53         self.title = title
54         self.peak_hold = peak_hold
55         self.input_is_real = input_is_real
56         self.msgq = gr.msg_queue(2)         # queue that holds a maximum of 2 messages
57
58     def set_y_per_div(self, y_per_div):
59         self.y_per_div = y_per_div
60
61     def set_ref_level(self, ref_level):
62         self.ref_level = ref_level
63
64     def set_average(self, average):
65         self.average = average
66         if average:
67             self.avg.set_taps(self.avg_alpha)
68             self.set_peak_hold(False)
69         else:
70             self.avg.set_taps(1.0)
71
72     def set_peak_hold(self, enable):
73         self.peak_hold = enable
74         if enable:
75             self.set_average(False)
76         self.win.set_peak_hold(enable)
77
78     def set_avg_alpha(self, avg_alpha):
79         self.avg_alpha = avg_alpha
80
81     def set_baseband_freq(self, baseband_freq):
82         self.baseband_freq = baseband_freq
83
84     def set_sample_rate(self, sample_rate):
85         self.sample_rate = sample_rate
86         self._set_n()
87
88     def _set_n(self):
89         self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
90         
91
92 class fft_sink_f(gr.hier_block2, fft_sink_base):
93     def __init__(self, parent, baseband_freq=0,
94                  y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
95                  fft_rate=default_fft_rate, average=False, avg_alpha=None,
96                  title='', size=default_fftsink_size, peak_hold=False):
97
98         gr.hier_block2.__init__(self, "fft_sink_f",
99                                 gr.io_signature(1, 1, gr.sizeof_float),
100                                 gr.io_signature(0,0,0))
101
102         fft_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
103                                y_per_div=y_per_div, ref_level=ref_level,
104                                sample_rate=sample_rate, fft_size=fft_size,
105                                fft_rate=fft_rate,
106                                average=average, avg_alpha=avg_alpha, title=title,
107                                peak_hold=peak_hold)
108                                
109         self.s2p = gr.stream_to_vector(gr.sizeof_float, self.fft_size)
110         self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
111                                          max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
112         
113         mywindow = window.blackmanharris(self.fft_size)
114         self.fft = gr.fft_vfc(self.fft_size, True, mywindow)
115         power = 0
116         for tap in mywindow:
117             power += tap*tap
118             
119         self.c2mag = gr.complex_to_mag(self.fft_size)
120         self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
121
122         # FIXME  We need to add 3dB to all bins but the DC bin
123         self.log = gr.nlog10_ff(20, self.fft_size,
124                                -20*math.log10(self.fft_size)-10*math.log10(power/self.fft_size))
125         self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
126         self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
127
128         self.win = fft_window(self, parent, size=size)
129         self.set_average(self.average)
130
131
132 class fft_sink_c(gr.hier_block2, fft_sink_base):
133     def __init__(self, parent, baseband_freq=0,
134                  y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
135                  fft_rate=default_fft_rate, average=False, avg_alpha=None,
136                  title='', size=default_fftsink_size, peak_hold=False):
137
138         gr.hier_block2.__init__(self, "fft_sink_c",
139                                 gr.io_signature(1, 1, gr.sizeof_gr_complex),
140                                 gr.io_signature(0,0,0))
141
142         fft_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
143                                y_per_div=y_per_div, ref_level=ref_level,
144                                sample_rate=sample_rate, fft_size=fft_size,
145                                fft_rate=fft_rate,
146                                average=average, avg_alpha=avg_alpha, title=title,
147                                peak_hold=peak_hold)
148
149         self.s2p = gr.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
150         self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
151                                          max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
152         
153         mywindow = window.blackmanharris(self.fft_size)
154         self.fft = gr.fft_vcc(self.fft_size, True, mywindow)
155         power = 0
156         for tap in mywindow:
157             power += tap*tap
158             
159         self.c2mag = gr.complex_to_mag(self.fft_size)
160         self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
161
162         # FIXME  We need to add 3dB to all bins but the DC bin
163         self.log = gr.nlog10_ff(20, self.fft_size,
164                                 -20*math.log10(self.fft_size)-10*math.log10(power/self.fft_size))
165         self.sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
166         self.connect(self, self.s2p, self.one_in_n, self.fft, self.c2mag, self.avg, self.log, self.sink)
167
168         self.win = fft_window(self, parent, size=size)
169         self.set_average(self.average)
170
171
172 # ------------------------------------------------------------------------
173
174 myDATA_EVENT = wx.NewEventType()
175 EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
176
177
178 class DataEvent(wx.PyEvent):
179     def __init__(self, data):
180         wx.PyEvent.__init__(self)
181         self.SetEventType (myDATA_EVENT)
182         self.data = data
183
184     def Clone (self): 
185         self.__class__ (self.GetId())
186
187
188 class input_watcher (threading.Thread):
189     def __init__ (self, msgq, fft_size, event_receiver, **kwds):
190         threading.Thread.__init__ (self, **kwds)
191         self.setDaemon (1)
192         self.msgq = msgq
193         self.fft_size = fft_size
194         self.event_receiver = event_receiver
195         self.keep_running = True
196         self.start ()
197
198     def run (self):
199         while (self.keep_running):
200             msg = self.msgq.delete_head()  # blocking read of message queue
201             itemsize = int(msg.arg1())
202             nitems = int(msg.arg2())
203
204             s = msg.to_string()            # get the body of the msg as a string
205
206             # There may be more than one FFT frame in the message.
207             # If so, we take only the last one
208             if nitems > 1:
209                 start = itemsize * (nitems - 1)
210                 s = s[start:start+itemsize]
211
212             complex_data = numpy.fromstring (s, numpy.float32)
213             de = DataEvent (complex_data)
214             wx.PostEvent (self.event_receiver, de)
215             del de
216     
217
218 class fft_window (plot.PlotCanvas):
219     def __init__ (self, fftsink, parent, id = -1,
220                   pos = wx.DefaultPosition, size = wx.DefaultSize,
221                   style = wx.DEFAULT_FRAME_STYLE, name = ""):
222         plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
223
224         self.y_range = None
225         self.fftsink = fftsink
226         self.peak_hold = False
227         self.peak_vals = None
228         
229         self.SetEnableGrid (True)
230         # self.SetEnableZoom (True)
231         # self.SetBackgroundColour ('black')
232         
233         self.build_popup_menu()
234         self.set_baseband_freq(0.0)
235                 
236         EVT_DATA_EVENT (self, self.set_data)
237         wx.EVT_CLOSE (self, self.on_close_window)
238         self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
239         self.Bind(wx.EVT_MOTION, self.evt_motion)
240         
241         self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
242
243     def set_scale(self, freq):
244         x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq)) 
245         if x >= 1e9:
246             self._scale_factor = 1e-9
247             self._units = "GHz"
248             self._format = "%3.6f"
249         elif x >= 1e6:
250             self._scale_factor = 1e-6
251             self._units = "MHz"
252             self._format = "%3.3f"
253         else:
254             self._scale_factor = 1e-3
255             self._units = "kHz"
256             self._format = "%3.3f"
257
258     def set_baseband_freq(self, baseband_freq):
259         self.set_scale(baseband_freq)
260         self.fftsink.set_baseband_freq(baseband_freq)
261         
262     def on_close_window (self, event):
263         print "fft_window:on_close_window"
264         self.keep_running = False
265
266
267     def set_data (self, evt):
268         dB = evt.data
269         L = len (dB)
270
271         if self.peak_hold:
272             if self.peak_vals is None:
273                 self.peak_vals = dB
274             else:
275                 self.peak_vals = numpy.maximum(dB, self.peak_vals)
276                 dB = self.peak_vals
277
278         if self.fftsink.input_is_real:     # only plot 1/2 the points
279             x_vals = ((numpy.arange (L/2) * (self.fftsink.sample_rate 
280                        * self._scale_factor / L))
281                       + self.fftsink.baseband_freq * self._scale_factor)
282             self._points = numpy.zeros((len(x_vals), 2), numpy.float64)
283             self._points[:,0] = x_vals
284             self._points[:,1] = dB[0:L/2]
285         else:
286             # the "negative freqs" are in the second half of the array
287             x_vals = ((numpy.arange (-L/2, L/2)
288                        * (self.fftsink.sample_rate * self._scale_factor / L))
289                       + self.fftsink.baseband_freq * self._scale_factor)
290             self._points = numpy.zeros((len(x_vals), 2), numpy.float64)
291             self._points[:,0] = x_vals
292             self._points[:,1] = numpy.concatenate ((dB[L/2:], dB[0:L/2]))
293
294         lines = plot.PolyLine (self._points, colour='BLUE')
295         graphics = plot.PlotGraphics ([lines],
296                                       title=self.fftsink.title,
297                                       xLabel = self._units, yLabel = "dB")
298
299         self.Draw (graphics, xAxis=None, yAxis=self.y_range)
300         self.update_y_range ()
301
302
303     def set_peak_hold(self, enable):
304         self.peak_hold = enable
305         self.peak_vals = None
306
307     def update_y_range (self):
308         ymax = self.fftsink.ref_level
309         ymin = self.fftsink.ref_level - self.fftsink.y_per_div * self.fftsink.y_divs
310         self.y_range = self._axisInterval ('min', ymin, ymax)
311
312     def on_average(self, evt):
313         # print "on_average"
314         self.fftsink.set_average(evt.IsChecked())
315
316     def on_peak_hold(self, evt):
317         # print "on_peak_hold"
318         self.fftsink.set_peak_hold(evt.IsChecked())
319
320     def on_incr_ref_level(self, evt):
321         # print "on_incr_ref_level"
322         self.fftsink.set_ref_level(self.fftsink.ref_level
323                                    + self.fftsink.y_per_div)
324
325     def on_decr_ref_level(self, evt):
326         # print "on_decr_ref_level"
327         self.fftsink.set_ref_level(self.fftsink.ref_level
328                                    - self.fftsink.y_per_div)
329
330     def on_incr_y_per_div(self, evt):
331         # print "on_incr_y_per_div"
332         self.fftsink.set_y_per_div(next_up(self.fftsink.y_per_div, (1,2,5,10,20)))
333
334     def on_decr_y_per_div(self, evt):
335         # print "on_decr_y_per_div"
336         self.fftsink.set_y_per_div(next_down(self.fftsink.y_per_div, (1,2,5,10,20)))
337
338     def on_y_per_div(self, evt):
339         # print "on_y_per_div"
340         Id = evt.GetId()
341         if Id == self.id_y_per_div_1:
342             self.fftsink.set_y_per_div(1)
343         elif Id == self.id_y_per_div_2:
344             self.fftsink.set_y_per_div(2)
345         elif Id == self.id_y_per_div_5:
346             self.fftsink.set_y_per_div(5)
347         elif Id == self.id_y_per_div_10:
348             self.fftsink.set_y_per_div(10)
349         elif Id == self.id_y_per_div_20:
350             self.fftsink.set_y_per_div(20)
351
352     def on_right_click(self, event):
353         menu = self.popup_menu
354         for id, pred in self.checkmarks.items():
355             item = menu.FindItemById(id)
356             item.Check(pred())
357         self.PopupMenu(menu, event.GetPosition())
358
359     def evt_motion(self, event):
360         # Clip to plotted values
361         (ux, uy) = self.GetXY(event)      # Scaled position
362         x_vals = numpy.array(self._points[:,0])
363         if ux < x_vals[0] or ux > x_vals[-1]:
364             tip = self.GetToolTip()
365             if tip:
366                 tip.Enable(False)
367             return
368
369         # Get nearest X value (is there a better way)?
370         ind = numpy.argmin(numpy.abs(x_vals-ux))
371         x_val = x_vals[ind]
372         db_val = self._points[ind, 1]
373         text = (self._format+" %s dB=%3.3f") % (x_val, self._units, db_val)
374
375         # Display the tooltip
376         tip = wx.ToolTip(text)
377         tip.Enable(True)
378         tip.SetDelay(0)
379         self.SetToolTip(tip)
380         
381     def build_popup_menu(self):
382         self.id_incr_ref_level = wx.NewId()
383         self.id_decr_ref_level = wx.NewId()
384         self.id_incr_y_per_div = wx.NewId()
385         self.id_decr_y_per_div = wx.NewId()
386         self.id_y_per_div_1 = wx.NewId()
387         self.id_y_per_div_2 = wx.NewId()
388         self.id_y_per_div_5 = wx.NewId()
389         self.id_y_per_div_10 = wx.NewId()
390         self.id_y_per_div_20 = wx.NewId()
391         self.id_average = wx.NewId()
392         self.id_peak_hold = wx.NewId()
393         
394         self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
395         self.Bind(wx.EVT_MENU, self.on_peak_hold, id=self.id_peak_hold)
396         self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
397         self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
398         self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
399         self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
400         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
401         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
402         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
403         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
404         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
405         
406         # make a menu
407         menu = wx.Menu()
408         self.popup_menu = menu
409         menu.AppendCheckItem(self.id_average, "Average")
410         menu.AppendCheckItem(self.id_peak_hold, "Peak Hold")
411         menu.Append(self.id_incr_ref_level, "Incr Ref Level")
412         menu.Append(self.id_decr_ref_level, "Decr Ref Level")
413         # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
414         # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
415         menu.AppendSeparator()
416         # we'd use RadioItems for these, but they're not supported on Mac
417         menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
418         menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
419         menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
420         menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
421         menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
422
423         self.checkmarks = {
424             self.id_average : lambda : self.fftsink.average,
425             self.id_peak_hold : lambda : self.fftsink.peak_hold,
426             self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
427             self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
428             self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
429             self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
430             self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
431             }
432
433
434 def next_up(v, seq):
435     """
436     Return the first item in seq that is > v.
437     """
438     for s in seq:
439         if s > v:
440             return s
441     return v
442
443 def next_down(v, seq):
444     """
445     Return the last item in seq that is < v.
446     """
447     rseq = list(seq[:])
448     rseq.reverse()
449
450     for s in rseq:
451         if s < v:
452             return s
453     return v
454
455
456 # ----------------------------------------------------------------
457 # Standalone test app
458 # ----------------------------------------------------------------
459
460 class test_app_block (stdgui2.std_top_block):
461     def __init__(self, frame, panel, vbox, argv):
462         stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
463
464         fft_size = 256
465
466         # build our flow graph
467         input_rate = 20.48e3
468
469         # Generate a complex sinusoid
470         #src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
471         src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
472
473         # We add these throttle blocks so that this demo doesn't
474         # suck down all the CPU available.  Normally you wouldn't use these.
475         thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
476
477         sink1 = fft_sink_c (panel, title="Complex Data", fft_size=fft_size,
478                             sample_rate=input_rate, baseband_freq=100e3,
479                             ref_level=0, y_per_div=20)
480         vbox.Add (sink1.win, 1, wx.EXPAND)
481
482         self.connect(src1, thr1, sink1)
483
484         #src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
485         src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
486         thr2 = gr.throttle(gr.sizeof_float, input_rate)
487         sink2 = fft_sink_f (panel, title="Real Data", fft_size=fft_size*2,
488                             sample_rate=input_rate, baseband_freq=100e3,
489                             ref_level=0, y_per_div=20)
490         vbox.Add (sink2.win, 1, wx.EXPAND)
491
492         self.connect(src2, thr2, sink2)
493
494 def main ():
495     app = stdgui2.stdapp (test_app_block, "FFT Sink Test App")
496     app.MainLoop ()
497
498 if __name__ == '__main__':
499     main ()