Add analog CRT screen afterglow emulation for gr-wxgui
[debian/gnuradio] / gr-wxgui / src / python / fft_window.py
1 #
2 # Copyright 2008, 2009 Free Software Foundation, Inc.
3 #
4 # This file is part of GNU Radio
5 #
6 # GNU Radio is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3, or (at your option)
9 # any later version.
10 #
11 # GNU Radio 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 GNU Radio; see the file COPYING.  If not, write to
18 # the Free Software Foundation, Inc., 51 Franklin Street,
19 # Boston, MA 02110-1301, USA.
20 #
21
22 ##################################################
23 # Imports
24 ##################################################
25 import plotter
26 import common
27 import wx
28 import numpy
29 import math
30 import pubsub
31 from constants import *
32 from gnuradio import gr #for gr.prefs
33 import forms
34
35 ##################################################
36 # Constants
37 ##################################################
38 SLIDER_STEPS = 100
39 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
40 ANALOG_ALPHA_MIN_EXP, ANALOG_ALPHA_MAX_EXP = -2, 0
41 DEFAULT_WIN_SIZE = (600, 300)
42 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30)
43 DB_DIV_MIN, DB_DIV_MAX = 1, 20
44 FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0)
45 PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0)
46 EMPTY_TRACE = list()
47 TRACES = ('A', 'B')
48 TRACES_COLOR_SPEC = {
49         'A': (1.0, 0.0, 0.0),
50         'B': (0.8, 0.0, 0.8),
51 }
52
53 ##################################################
54 # FFT window control panel
55 ##################################################
56 class control_panel(wx.Panel):
57         """
58         A control panel with wx widgits to control the plotter and fft block chain.
59         """
60
61         def __init__(self, parent):
62                 """
63                 Create a new control panel.
64                 @param parent the wx parent window
65                 """
66                 self.parent = parent
67                 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
68                 control_box = wx.BoxSizer(wx.VERTICAL)
69                 control_box.AddStretchSpacer()
70                 #checkboxes for average and peak hold
71                 options_box = forms.static_box_sizer(
72                         parent=self, sizer=control_box, label='Trace Options',
73                         bold=True, orient=wx.VERTICAL,
74                 )
75                 forms.check_box(
76                         sizer=options_box, parent=self, label='Peak Hold',
77                         ps=parent, key=PEAK_HOLD_KEY,
78                 )
79                 forms.check_box(
80                         sizer=options_box, parent=self, label='Average',
81                         ps=parent, key=AVERAGE_KEY,
82                 )
83                 #static text and slider for averaging
84                 avg_alpha_text = forms.static_text(
85                         sizer=options_box, parent=self, label='Avg Alpha',
86                         converter=forms.float_converter(lambda x: '%.4f'%x),
87                         ps=parent, key=AVG_ALPHA_KEY, width=50,
88                 )
89                 avg_alpha_slider = forms.log_slider(
90                         sizer=options_box, parent=self,
91                         min_exp=AVG_ALPHA_MIN_EXP,
92                         max_exp=AVG_ALPHA_MAX_EXP,
93                         num_steps=SLIDER_STEPS,
94                         ps=parent, key=AVG_ALPHA_KEY,
95                 )
96                 for widget in (avg_alpha_text, avg_alpha_slider):
97                         parent.subscribe(AVERAGE_KEY, widget.Enable)
98                         widget.Enable(parent[AVERAGE_KEY])
99                         parent.subscribe(AVERAGE_KEY, widget.ShowItems)
100                         #allways show initially, so room is reserved for them
101                         widget.ShowItems(True) # (parent[AVERAGE_KEY])
102
103                 parent.subscribe(AVERAGE_KEY, self._update_layout)
104
105                 forms.check_box(
106                         sizer=options_box, parent=self, label='Emulate Analog',
107                         ps=parent, key=EMULATE_ANALOG_KEY,
108                 )
109                 #static text and slider for analog alpha
110                 analog_alpha_text = forms.static_text(
111                         sizer=options_box, parent=self, label='Analog Alpha',
112                         converter=forms.float_converter(lambda x: '%.4f'%x),
113                         ps=parent, key=ANALOG_ALPHA_KEY, width=50,
114                 )
115                 analog_alpha_slider = forms.log_slider(
116                         sizer=options_box, parent=self,
117                         min_exp=ANALOG_ALPHA_MIN_EXP,
118                         max_exp=ANALOG_ALPHA_MAX_EXP,
119                         num_steps=SLIDER_STEPS,
120                         ps=parent, key=ANALOG_ALPHA_KEY,
121                 )
122                 for widget in (analog_alpha_text, analog_alpha_slider):
123                         parent.subscribe(EMULATE_ANALOG_KEY, widget.Enable)
124                         widget.Enable(parent[EMULATE_ANALOG_KEY])
125                         parent.subscribe(EMULATE_ANALOG_KEY, widget.ShowItems)
126                         #allways show initially, so room is reserved for them
127                         widget.ShowItems(True) # (parent[EMULATE_ANALOG_KEY])
128                 
129                 parent.subscribe(EMULATE_ANALOG_KEY, self._update_layout)
130
131                 #trace menu
132                 for trace in TRACES:
133                         trace_box = wx.BoxSizer(wx.HORIZONTAL)
134                         options_box.Add(trace_box, 0, wx.EXPAND)
135                         forms.check_box(
136                                 sizer=trace_box, parent=self,
137                                 ps=parent, key=TRACE_SHOW_KEY+trace,
138                                 label='Trace %s'%trace,
139                         )
140                         trace_box.AddSpacer(10)
141                         forms.single_button(
142                                 sizer=trace_box, parent=self,
143                                 ps=parent, key=TRACE_STORE_KEY+trace,
144                                 label='Store', style=wx.BU_EXACTFIT,
145                         )
146                         trace_box.AddSpacer(10)
147                 #radio buttons for div size
148                 control_box.AddStretchSpacer()
149                 y_ctrl_box = forms.static_box_sizer(
150                         parent=self, sizer=control_box, label='Axis Options',
151                         bold=True, orient=wx.VERTICAL,
152                 )
153                 forms.incr_decr_buttons(
154                         parent=self, sizer=y_ctrl_box, label='dB/Div',
155                         on_incr=self._on_incr_db_div, on_decr=self._on_decr_db_div,
156                 )
157                 #ref lvl buttons
158                 forms.incr_decr_buttons(
159                         parent=self, sizer=y_ctrl_box, label='Ref Level',
160                         on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
161                 )
162                 y_ctrl_box.AddSpacer(2)
163                 #autoscale
164                 forms.single_button(
165                         sizer=y_ctrl_box, parent=self, label='Autoscale',
166                         callback=self.parent.autoscale,
167                 )
168                 #run/stop
169                 control_box.AddStretchSpacer()
170                 forms.toggle_button(
171                         sizer=control_box, parent=self,
172                         true_label='Stop', false_label='Run',
173                         ps=parent, key=RUNNING_KEY,
174                 )
175                 #set sizer
176                 self.SetSizerAndFit(control_box)
177
178                 #mouse wheel event
179                 def on_mouse_wheel(event):
180                         if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
181                         else: self._on_decr_ref_level(event)
182                 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
183
184         ##################################################
185         # Event handlers
186         ##################################################
187         def _on_incr_ref_level(self, event):
188                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
189         def _on_decr_ref_level(self, event):
190                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
191         def _on_incr_db_div(self, event):
192                 self.parent[Y_PER_DIV_KEY] = min(DB_DIV_MAX, common.get_clean_incr(self.parent[Y_PER_DIV_KEY]))
193         def _on_decr_db_div(self, event):
194                 self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, common.get_clean_decr(self.parent[Y_PER_DIV_KEY]))
195         ##################################################
196         # subscriber handlers
197         ##################################################
198         def _update_layout(self,key):
199           # Just ignore the key value we get
200           # we only need to now that the visability or size of something has changed
201           self.parent.Layout()
202           #self.parent.Fit()          
203
204 ##################################################
205 # FFT window with plotter and control panel
206 ##################################################
207 class fft_window(wx.Panel, pubsub.pubsub):
208         def __init__(
209                 self,
210                 parent,
211                 controller,
212                 size,
213                 title,
214                 real,
215                 fft_size,
216                 baseband_freq,
217                 sample_rate_key,
218                 y_per_div,
219                 y_divs,
220                 ref_level,
221                 average_key,
222                 avg_alpha_key,
223                 peak_hold,
224                 msg_key,
225                 emulate_analog,
226                 analog_alpha,
227         ):
228
229                 pubsub.pubsub.__init__(self)
230                 #setup
231                 self.samples = EMPTY_TRACE
232                 self.real = real
233                 self.fft_size = fft_size
234                 self._reset_peak_vals()
235                 self._traces = dict()
236                 #proxy the keys
237                 self.proxy(MSG_KEY, controller, msg_key)
238                 self.proxy(AVERAGE_KEY, controller, average_key)
239                 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
240                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
241                 #initialize values
242                 self[PEAK_HOLD_KEY] = peak_hold
243                 self[Y_PER_DIV_KEY] = y_per_div
244                 self[Y_DIVS_KEY] = y_divs
245                 self[X_DIVS_KEY] = 8 #approximate
246                 self[REF_LEVEL_KEY] = ref_level
247                 self[BASEBAND_FREQ_KEY] = baseband_freq
248                 self[RUNNING_KEY] = True
249                 self[EMULATE_ANALOG_KEY] = emulate_analog
250                 self[ANALOG_ALPHA_KEY] = analog_alpha
251                 for trace in TRACES:
252                         #a function that returns a function
253                         #so the function wont use local trace
254                         def new_store_trace(my_trace):
255                                 def store_trace(*args):
256                                         self._traces[my_trace] = self.samples
257                                         self.update_grid()
258                                 return store_trace
259                         def new_toggle_trace(my_trace):
260                                 def toggle_trace(toggle):
261                                         #do an automatic store if toggled on and empty trace
262                                         if toggle and not len(self._traces[my_trace]):
263                                                 self._traces[my_trace] = self.samples
264                                         self.update_grid()
265                                 return toggle_trace
266                         self._traces[trace] = EMPTY_TRACE
267                         self[TRACE_STORE_KEY+trace] = False
268                         self[TRACE_SHOW_KEY+trace] = False
269                         self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace))
270                         self.subscribe(TRACE_SHOW_KEY+trace, new_toggle_trace(trace))
271                 #init panel and plot
272                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
273                 self.plotter = plotter.channel_plotter(self)
274                 self.plotter.SetSize(wx.Size(*size))
275                 self.plotter.set_title(title)
276                 self.plotter.enable_legend(True)
277                 self.plotter.enable_point_label(True)
278                 self.plotter.enable_grid_lines(True)
279                 self.plotter.set_emulate_analog(emulate_analog)
280                 self.plotter.set_analog_alpha(analog_alpha)
281                 #setup the box with plot and controls
282                 self.control_panel = control_panel(self)
283                 main_box = wx.BoxSizer(wx.HORIZONTAL)
284                 main_box.Add(self.plotter, 1, wx.EXPAND)
285                 main_box.Add(self.control_panel, 0, wx.EXPAND)
286                 self.SetSizerAndFit(main_box)
287                 #register events
288                 self.subscribe(AVERAGE_KEY, self._reset_peak_vals)
289                 self.subscribe(MSG_KEY, self.handle_msg)
290                 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
291                 for key in (
292                         BASEBAND_FREQ_KEY,
293                         Y_PER_DIV_KEY, X_DIVS_KEY,
294                         Y_DIVS_KEY, REF_LEVEL_KEY,
295                 ): self.subscribe(key, self.update_grid)
296                 self.subscribe(EMULATE_ANALOG_KEY, self.plotter.set_emulate_analog)
297                 self.subscribe(ANALOG_ALPHA_KEY, self.plotter.set_analog_alpha)
298                 #initial update
299                 self.update_grid()
300
301
302         def autoscale(self, *args):
303                 """
304                 Autoscale the fft plot to the last frame.
305                 Set the dynamic range and reference level.
306                 """
307                 if not len(self.samples): return
308                 min_level, max_level = common.get_min_max_fft(self.samples)
309                 #set the range to a clean number of the dynamic range
310                 self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
311                 #set the reference level to a multiple of y per div
312                 self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
313
314         def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
315
316         def handle_msg(self, msg):
317                 """
318                 Handle the message from the fft sink message queue.
319                 If complex, reorder the fft samples so the negative bins come first.
320                 If real, keep take only the positive bins.
321                 Plot the samples onto the grid as channel 1.
322                 If peak hold is enabled, plot peak vals as channel 2.
323                 @param msg the fft array as a character array
324                 """
325                 if not self[RUNNING_KEY]: return
326                 #convert to floating point numbers
327                 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
328                 num_samps = len(samples)
329                 #reorder fft
330                 if self.real: samples = samples[:(num_samps+1)/2]
331                 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
332                 self.samples = samples
333                 #peak hold calculation
334                 if self[PEAK_HOLD_KEY]:
335                         if len(self.peak_vals) != len(samples): self.peak_vals = samples
336                         self.peak_vals = numpy.maximum(samples, self.peak_vals)
337                         #plot the peak hold
338                         self.plotter.set_waveform(
339                                 channel='Peak',
340                                 samples=self.peak_vals,
341                                 color_spec=PEAK_VALS_COLOR_SPEC,
342                         )
343                 else:
344                         self._reset_peak_vals()
345                         self.plotter.clear_waveform(channel='Peak')
346                 #plot the fft
347                 self.plotter.set_waveform(
348                         channel='FFT',
349                         samples=samples,
350                         color_spec=FFT_PLOT_COLOR_SPEC,
351                 )
352                 #update the plotter
353                 self.plotter.update()
354
355         def update_grid(self, *args):
356                 """
357                 Update the plotter grid.
358                 This update method is dependent on the variables below.
359                 Determine the x and y axis grid parameters.
360                 The x axis depends on sample rate, baseband freq, and x divs.
361                 The y axis depends on y per div, y divs, and ref level.
362                 """
363                 for trace in TRACES:
364                         channel = '%s'%trace.upper()
365                         if self[TRACE_SHOW_KEY+trace]:
366                                 self.plotter.set_waveform(
367                                         channel=channel,
368                                         samples=self._traces[trace],
369                                         color_spec=TRACES_COLOR_SPEC[trace],
370                                 )
371                         else: self.plotter.clear_waveform(channel=channel)
372                 #grid parameters
373                 sample_rate = self[SAMPLE_RATE_KEY]
374                 baseband_freq = self[BASEBAND_FREQ_KEY]
375                 y_per_div = self[Y_PER_DIV_KEY]
376                 y_divs = self[Y_DIVS_KEY]
377                 x_divs = self[X_DIVS_KEY]
378                 ref_level = self[REF_LEVEL_KEY]
379                 #determine best fitting x_per_div
380                 if self.real: x_width = sample_rate/2.0
381                 else: x_width = sample_rate/1.0
382                 x_per_div = common.get_clean_num(x_width/x_divs)
383                 #update the x grid
384                 if self.real:
385                         self.plotter.set_x_grid(
386                                 baseband_freq,
387                                 baseband_freq + sample_rate/2.0,
388                                 x_per_div, True,
389                         )
390                 else:
391                         self.plotter.set_x_grid(
392                                 baseband_freq - sample_rate/2.0,
393                                 baseband_freq + sample_rate/2.0,
394                                 x_per_div, True,
395                         )
396                 #update x units
397                 self.plotter.set_x_label('Frequency', 'Hz')
398                 #update y grid
399                 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
400                 #update y units
401                 self.plotter.set_y_label('Amplitude', 'dB')
402                 #update plotter
403                 self.plotter.update()