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