Imported Upstream version 3.0
[debian/gnuradio] / gr-wxgui / src / python / fftsink.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2003,2004,2005,2006 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 2, 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 stdgui
25 import wx
26 import gnuradio.wxgui.plot as plot
27 import Numeric
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_block, fft_sink_base):
93     def __init__(self, fg, 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         fft_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
99                                y_per_div=y_per_div, ref_level=ref_level,
100                                sample_rate=sample_rate, fft_size=fft_size,
101                                fft_rate=fft_rate,
102                                average=average, avg_alpha=avg_alpha, title=title,
103                                peak_hold=peak_hold)
104                                
105         s2p = gr.stream_to_vector(gr.sizeof_float, self.fft_size)
106         self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
107                                          max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
108
109         mywindow = window.blackmanharris(self.fft_size)
110         fft = gr.fft_vfc(self.fft_size, True, mywindow)
111         power = 0
112         for tap in mywindow:
113             power += tap*tap
114             
115         c2mag = gr.complex_to_mag(self.fft_size)
116         self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
117
118         # FIXME  We need to add 3dB to all bins but the DC bin
119         log = gr.nlog10_ff(20, self.fft_size,
120                            -20*math.log10(self.fft_size)-10*math.log10(power/self.fft_size))
121         sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
122
123         fg.connect (s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
124         gr.hier_block.__init__(self, fg, s2p, sink)
125
126         self.win = fft_window(self, parent, size=size)
127         self.set_average(self.average)
128
129
130 class fft_sink_c(gr.hier_block, fft_sink_base):
131     def __init__(self, fg, parent, baseband_freq=0,
132                  y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
133                  fft_rate=default_fft_rate, average=False, avg_alpha=None,
134                  title='', size=default_fftsink_size, peak_hold=False):
135
136         fft_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
137                                y_per_div=y_per_div, ref_level=ref_level,
138                                sample_rate=sample_rate, fft_size=fft_size,
139                                fft_rate=fft_rate,
140                                average=average, avg_alpha=avg_alpha, title=title,
141                                peak_hold=peak_hold)
142
143         s2p = gr.stream_to_vector(gr.sizeof_gr_complex, self.fft_size)
144         self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
145                                          max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
146         mywindow = window.blackmanharris(self.fft_size)
147         power = 0
148         for tap in mywindow:
149             power += tap*tap
150             
151         fft = gr.fft_vcc(self.fft_size, True, mywindow)
152         c2mag = gr.complex_to_mag(fft_size)
153         self.avg = gr.single_pole_iir_filter_ff(1.0, fft_size)
154         log = gr.nlog10_ff(20, self.fft_size,
155                            -20*math.log10(self.fft_size)-10*math.log10(power/self.fft_size))
156         sink = gr.message_sink(gr.sizeof_float * fft_size, self.msgq, True)
157
158         fg.connect(s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
159         gr.hier_block.__init__(self, fg, s2p, sink)
160
161         self.win = fft_window(self, parent, size=size)
162         self.set_average(self.average)
163
164
165 # ------------------------------------------------------------------------
166
167 myDATA_EVENT = wx.NewEventType()
168 EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
169
170
171 class DataEvent(wx.PyEvent):
172     def __init__(self, data):
173         wx.PyEvent.__init__(self)
174         self.SetEventType (myDATA_EVENT)
175         self.data = data
176
177     def Clone (self): 
178         self.__class__ (self.GetId())
179
180
181 class input_watcher (threading.Thread):
182     def __init__ (self, msgq, fft_size, event_receiver, **kwds):
183         threading.Thread.__init__ (self, **kwds)
184         self.setDaemon (1)
185         self.msgq = msgq
186         self.fft_size = fft_size
187         self.event_receiver = event_receiver
188         self.keep_running = True
189         self.start ()
190
191     def run (self):
192         while (self.keep_running):
193             msg = self.msgq.delete_head()  # blocking read of message queue
194             itemsize = int(msg.arg1())
195             nitems = int(msg.arg2())
196
197             s = msg.to_string()            # get the body of the msg as a string
198
199             # There may be more than one FFT frame in the message.
200             # If so, we take only the last one
201             if nitems > 1:
202                 start = itemsize * (nitems - 1)
203                 s = s[start:start+itemsize]
204
205             complex_data = Numeric.fromstring (s, Numeric.Float32)
206             de = DataEvent (complex_data)
207             wx.PostEvent (self.event_receiver, de)
208             del de
209     
210
211 class fft_window (plot.PlotCanvas):
212     def __init__ (self, fftsink, parent, id = -1,
213                   pos = wx.DefaultPosition, size = wx.DefaultSize,
214                   style = wx.DEFAULT_FRAME_STYLE, name = ""):
215         plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
216
217         self.y_range = None
218         self.fftsink = fftsink
219         self.peak_hold = False
220         self.peak_vals = None
221
222         self.SetEnableGrid (True)
223         # self.SetEnableZoom (True)
224         # self.SetBackgroundColour ('black')
225         
226         self.build_popup_menu()
227         
228         EVT_DATA_EVENT (self, self.set_data)
229         wx.EVT_CLOSE (self, self.on_close_window)
230         self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
231
232         self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
233
234
235     def on_close_window (self, event):
236         print "fft_window:on_close_window"
237         self.keep_running = False
238
239
240     def set_data (self, evt):
241         dB = evt.data
242         L = len (dB)
243
244         if self.peak_hold:
245             if self.peak_vals is None:
246                 self.peak_vals = dB
247             else:
248                 self.peak_vals = Numeric.maximum(dB, self.peak_vals)
249                 dB = self.peak_vals
250
251         x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
252         if x >= 1e9:
253             sf = 1e-9
254             units = "GHz"
255         elif x >= 1e6:
256             sf = 1e-6
257             units = "MHz"
258         else:
259             sf = 1e-3
260             units = "kHz"
261
262         if self.fftsink.input_is_real:     # only plot 1/2 the points
263             x_vals = ((Numeric.arrayrange (L/2)
264                        * (self.fftsink.sample_rate * sf / L))
265                       + self.fftsink.baseband_freq * sf)
266             points = Numeric.zeros((len(x_vals), 2), Numeric.Float64)
267             points[:,0] = x_vals
268             points[:,1] = dB[0:L/2]
269         else:
270             # the "negative freqs" are in the second half of the array
271             x_vals = ((Numeric.arrayrange (-L/2, L/2)
272                        * (self.fftsink.sample_rate * sf / L))
273                       + self.fftsink.baseband_freq * sf)
274             points = Numeric.zeros((len(x_vals), 2), Numeric.Float64)
275             points[:,0] = x_vals
276             points[:,1] = Numeric.concatenate ((dB[L/2:], dB[0:L/2]))
277
278
279         lines = plot.PolyLine (points, colour='BLUE')
280
281         graphics = plot.PlotGraphics ([lines],
282                                       title=self.fftsink.title,
283                                       xLabel = units, yLabel = "dB")
284
285         self.Draw (graphics, xAxis=None, yAxis=self.y_range)
286         self.update_y_range ()
287
288     def set_peak_hold(self, enable):
289         self.peak_hold = enable
290         self.peak_vals = None
291
292     def update_y_range (self):
293         ymax = self.fftsink.ref_level
294         ymin = self.fftsink.ref_level - self.fftsink.y_per_div * self.fftsink.y_divs
295         self.y_range = self._axisInterval ('min', ymin, ymax)
296
297     def on_average(self, evt):
298         # print "on_average"
299         self.fftsink.set_average(evt.IsChecked())
300
301     def on_peak_hold(self, evt):
302         # print "on_peak_hold"
303         self.fftsink.set_peak_hold(evt.IsChecked())
304
305     def on_incr_ref_level(self, evt):
306         # print "on_incr_ref_level"
307         self.fftsink.set_ref_level(self.fftsink.ref_level
308                                    + self.fftsink.y_per_div)
309
310     def on_decr_ref_level(self, evt):
311         # print "on_decr_ref_level"
312         self.fftsink.set_ref_level(self.fftsink.ref_level
313                                    - self.fftsink.y_per_div)
314
315     def on_incr_y_per_div(self, evt):
316         # print "on_incr_y_per_div"
317         self.fftsink.set_y_per_div(next_up(self.fftsink.y_per_div, (1,2,5,10,20)))
318
319     def on_decr_y_per_div(self, evt):
320         # print "on_decr_y_per_div"
321         self.fftsink.set_y_per_div(next_down(self.fftsink.y_per_div, (1,2,5,10,20)))
322
323     def on_y_per_div(self, evt):
324         # print "on_y_per_div"
325         Id = evt.GetId()
326         if Id == self.id_y_per_div_1:
327             self.fftsink.set_y_per_div(1)
328         elif Id == self.id_y_per_div_2:
329             self.fftsink.set_y_per_div(2)
330         elif Id == self.id_y_per_div_5:
331             self.fftsink.set_y_per_div(5)
332         elif Id == self.id_y_per_div_10:
333             self.fftsink.set_y_per_div(10)
334         elif Id == self.id_y_per_div_20:
335             self.fftsink.set_y_per_div(20)
336
337         
338     def on_right_click(self, event):
339         menu = self.popup_menu
340         for id, pred in self.checkmarks.items():
341             item = menu.FindItemById(id)
342             item.Check(pred())
343         self.PopupMenu(menu, event.GetPosition())
344
345
346     def build_popup_menu(self):
347         self.id_incr_ref_level = wx.NewId()
348         self.id_decr_ref_level = wx.NewId()
349         self.id_incr_y_per_div = wx.NewId()
350         self.id_decr_y_per_div = wx.NewId()
351         self.id_y_per_div_1 = wx.NewId()
352         self.id_y_per_div_2 = wx.NewId()
353         self.id_y_per_div_5 = wx.NewId()
354         self.id_y_per_div_10 = wx.NewId()
355         self.id_y_per_div_20 = wx.NewId()
356         self.id_average = wx.NewId()
357         self.id_peak_hold = wx.NewId()
358
359         self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
360         self.Bind(wx.EVT_MENU, self.on_peak_hold, id=self.id_peak_hold)
361         self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
362         self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
363         self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
364         self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
365         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
366         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
367         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
368         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
369         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
370
371
372         # make a menu
373         menu = wx.Menu()
374         self.popup_menu = menu
375         menu.AppendCheckItem(self.id_average, "Average")
376         menu.AppendCheckItem(self.id_peak_hold, "Peak Hold")
377         menu.Append(self.id_incr_ref_level, "Incr Ref Level")
378         menu.Append(self.id_decr_ref_level, "Decr Ref Level")
379         # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
380         # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
381         menu.AppendSeparator()
382         # we'd use RadioItems for these, but they're not supported on Mac
383         menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
384         menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
385         menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
386         menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
387         menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
388
389         self.checkmarks = {
390             self.id_average : lambda : self.fftsink.average,
391             self.id_peak_hold : lambda : self.fftsink.peak_hold,
392             self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
393             self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
394             self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
395             self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
396             self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
397             }
398
399
400 def next_up(v, seq):
401     """
402     Return the first item in seq that is > v.
403     """
404     for s in seq:
405         if s > v:
406             return s
407     return v
408
409 def next_down(v, seq):
410     """
411     Return the last item in seq that is < v.
412     """
413     rseq = list(seq[:])
414     rseq.reverse()
415
416     for s in rseq:
417         if s < v:
418             return s
419     return v
420
421
422 # ----------------------------------------------------------------
423 #                     Deprecated interfaces
424 # ----------------------------------------------------------------
425
426 # returns (block, win).
427 #   block requires a single input stream of float
428 #   win is a subclass of wxWindow
429
430 def make_fft_sink_f(fg, parent, title, fft_size, input_rate, ymin = 0, ymax=50):
431     
432     block = fft_sink_f(fg, parent, title=title, fft_size=fft_size, sample_rate=input_rate,
433                        y_per_div=(ymax - ymin)/8, ref_level=ymax)
434     return (block, block.win)
435
436 # returns (block, win).
437 #   block requires a single input stream of gr_complex
438 #   win is a subclass of wxWindow
439
440 def make_fft_sink_c(fg, parent, title, fft_size, input_rate, ymin=0, ymax=50):
441     block = fft_sink_c(fg, parent, title=title, fft_size=fft_size, sample_rate=input_rate,
442                        y_per_div=(ymax - ymin)/8, ref_level=ymax)
443     return (block, block.win)
444
445
446 # ----------------------------------------------------------------
447 # Standalone test app
448 # ----------------------------------------------------------------
449
450 class test_app_flow_graph (stdgui.gui_flow_graph):
451     def __init__(self, frame, panel, vbox, argv):
452         stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
453
454         fft_size = 256
455
456         # build our flow graph
457         input_rate = 20.48e3
458
459         # Generate a complex sinusoid
460         #src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
461         src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
462
463         # We add these throttle blocks so that this demo doesn't
464         # suck down all the CPU available.  Normally you wouldn't use these.
465         thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
466
467         sink1 = fft_sink_c (self, panel, title="Complex Data", fft_size=fft_size,
468                             sample_rate=input_rate, baseband_freq=100e3,
469                             ref_level=0, y_per_div=20)
470         vbox.Add (sink1.win, 1, wx.EXPAND)
471         self.connect (src1, thr1, sink1)
472
473         #src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
474         src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
475         thr2 = gr.throttle(gr.sizeof_float, input_rate)
476         sink2 = fft_sink_f (self, panel, title="Real Data", fft_size=fft_size*2,
477                             sample_rate=input_rate, baseband_freq=100e3,
478                             ref_level=0, y_per_div=20)
479         vbox.Add (sink2.win, 1, wx.EXPAND)
480         self.connect (src2, thr2, sink2)
481
482 def main ():
483     app = stdgui.stdapp (test_app_flow_graph,
484                          "FFT Sink Test App")
485     app.MainLoop ()
486
487 if __name__ == '__main__':
488     main ()