Updated license from GPL version 2 or later to GPL version 3 or later.
[debian/gnuradio] / gnuradio-examples / python / usrp / usrp_wfm_rcv_sca.py
1 #!/usr/bin/env python
2 #
3 # Copyright 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 along
18 # with this program; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #
21
22 """
23 Here is a bit of code that will receive SCA analog subcarriers of FM
24 Broadcast Stations using the USRP.  It is a modified version of
25 usrp_wfm_rcv.py.
26
27 Common SCA frequencies are 67 kHz and 92 kHz.  SCA is used for Reading
28 Services for the Blind, Background Music, Foreign Language Services, and
29 other services.  Remember you may hear static when tuned to a FM station
30 because this code only outputs SCA audio.
31
32 The USRP gain is critical for good decoding.  Adjust for minimum noise.
33  I use the Post FM Demod FFT to check for SCA subcarriers and to adjust
34 the USRP gain for the lowest noise floor.  The stereo pilot at 19 KHz,
35 the stereo difference signal around 38 KHz, and RDS at 57 KHz are also
36 displayed on the Post FM Demod FFT if present.
37
38 The range below 67 kHz is used for SCA only when Stereo is not used.
39
40 The SCA recieve range is not as far as the main FM carrier receive range
41 so tune in strong local stations first.
42
43 I tried to comment the code with the various parameters.  There seems to
44 be several choices for a couple of them.  I coded the common ones I see
45 here.
46
47 In the local area there are a couple of stations using digital SCA.
48 These look similar to narrow DRM signals and I wonder if they are using
49 OFDM.
50 """
51
52
53 from gnuradio import gr, gru, eng_notation, optfir
54 from gnuradio import audio
55 from gnuradio import usrp
56 from gnuradio import blks
57 from gnuradio.blksimpl.fm_emph import fm_deemph
58 from gnuradio.eng_option import eng_option
59 from gnuradio.wxgui import slider, powermate
60 from gnuradio.wxgui import stdgui, fftsink, form
61 from optparse import OptionParser
62 from usrpm import usrp_dbid
63 import sys
64 import math
65 import wx
66
67 def pick_subdevice(u):
68     """
69     The user didn't specify a subdevice on the command line.
70     Try for one of these, in order: TV_RX, BASIC_RX, whatever is on side A.
71
72     @return a subdev_spec
73     """
74     return usrp.pick_subdev(u, (usrp_dbid.TV_RX,
75                                 usrp_dbid.TV_RX_REV_2,
76                                 usrp_dbid.BASIC_RX))
77
78
79 class wfm_rx_sca_graph (stdgui.gui_flow_graph):
80     def __init__(self,frame,panel,vbox,argv):
81         stdgui.gui_flow_graph.__init__ (self,frame,panel,vbox,argv)
82
83         parser=OptionParser(option_class=eng_option)
84         parser.add_option("-R", "--rx-subdev-spec", type="subdev", default=None,
85                           help="select USRP Rx side A or B (default=A)")
86         parser.add_option("-f", "--freq", type="eng_float", default=100.1e6,
87                           help="set frequency to FREQ", metavar="FREQ")
88         parser.add_option("-g", "--gain", type="eng_float", default=40,
89                           help="set gain in dB (default is midpoint)")
90         parser.add_option("-V", "--volume", type="eng_float", default=None,
91                           help="set volume (default is midpoint)")
92         parser.add_option("-O", "--audio-output", type="string", default="",
93                           help="pcm device name.  E.g., hw:0,0 or surround51 or /dev/dsp")
94
95         (options, args) = parser.parse_args()
96         if len(args) != 0:
97             parser.print_help()
98             sys.exit(1)
99
100         self.frame = frame
101         self.panel = panel
102
103         self.vol = 0
104         self.state = "FREQ"
105         self.freq = 0
106
107         # build graph
108
109         self.u = usrp.source_c()                    # usrp is data source
110
111         adc_rate = self.u.adc_rate()                # 64 MS/s
112         usrp_decim = 200
113         self.u.set_decim_rate(usrp_decim)
114         usrp_rate = adc_rate / usrp_decim           # 320 kS/s
115         chanfilt_decim = 1
116         demod_rate = usrp_rate / chanfilt_decim
117         sca_chanfilt_decim = 5
118         sca_demod_rate = demod_rate / sca_chanfilt_decim  #64 kHz
119         audio_decimation = 2
120         audio_rate = sca_demod_rate / audio_decimation  # 32 kHz
121
122         if options.rx_subdev_spec is None:
123             options.rx_subdev_spec = pick_subdevice(self.u)
124
125         self.u.set_mux(usrp.determine_rx_mux_value(self.u, options.rx_subdev_spec))
126         self.subdev = usrp.selected_subdev(self.u, options.rx_subdev_spec)
127         print "Using RX d'board %s" % (self.subdev.side_and_name(),)
128
129         #Create filter to get main FM Channel we want
130         chan_filt_coeffs = optfir.low_pass (1,           # gain
131                                             usrp_rate,   # sampling rate
132                                             100e3,        # passband cutoff
133                                             140e3,       # stopband cutoff
134                                             0.1,         # passband ripple
135                                             60)          # stopband attenuation
136         #print len(chan_filt_coeffs)
137         chan_filt = gr.fir_filter_ccf (chanfilt_decim, chan_filt_coeffs)
138
139         #Create demodulator block for Main FM Channel
140         max_dev = 75e3
141         fm_demod_gain = demod_rate/(2*math.pi*max_dev)
142         self.fm_demod = gr.quadrature_demod_cf (fm_demod_gain)
143
144         # Note - deemphasis is not applied to the Main FM Channel as main audio is not decoded
145
146         # SCA Devation is 10% of carrier but some references say 20% if mono with one SCA (6 KHz seems typical)
147         max_sca_dev = 6e3
148
149         # Create filter to get SCA channel we want
150         sca_chan_coeffs = gr.firdes.low_pass (1.0,                # gain
151                                           demod_rate,       # sampling rate
152                                           max_sca_dev,      # low pass cutoff freq
153                                           max_sca_dev/3,    # width of trans. band
154                                           gr.firdes.WIN_HANN) # filter type
155
156         self.ddc = gr.freq_xlating_fir_filter_fcf(sca_chanfilt_decim,       # decimation rate
157                                                   sca_chan_coeffs,    # taps
158                                                   0,              # frequency translation amount (Gets set by the UI)
159                                                   demod_rate)   # input sample rate
160
161         #Create demodulator block for SCA Channel
162         sca_demod_gain = sca_demod_rate/(2*math.pi*max_sca_dev)
163         self.fm_demod_sca = gr.quadrature_demod_cf (sca_demod_gain)
164
165
166         # SCA analog audio is bandwidth limited to 5 KHz
167         max_sca_audio_freq = 5.0e3
168         # SCA analog deephasis is 150 uS (75 uS may be used)
169         sca_tau = 150e-6
170
171         # compute FIR filter taps for SCA audio filter
172         audio_coeffs = gr.firdes.low_pass (1.0,         # gain
173                                            sca_demod_rate,      # sampling rate
174                                            max_sca_audio_freq, # low pass cutoff freq
175                                            max_sca_audio_freq/2.5,             # width of trans. band
176                                            gr.firdes.WIN_HAMMING)
177
178         # input: float; output: float
179         self.audio_filter = gr.fir_filter_fff (audio_decimation, audio_coeffs)
180
181         # Create deemphasis block that is applied after SCA demodulation
182         self.deemph = fm_deemph (self, audio_rate, sca_tau)
183
184         self.volume_control = gr.multiply_const_ff(self.vol)
185
186         # sound card as final sink
187         audio_sink = audio.sink (int (audio_rate),
188                                  options.audio_output,
189                                  False)  # ok_to_block
190
191         # now wire it all together
192         self.connect (self.u, chan_filt, self.fm_demod, self.ddc, self.fm_demod_sca)
193         self.connect (self.fm_demod_sca, self.audio_filter, self.deemph, self.volume_control, audio_sink)
194
195         self._build_gui(vbox, usrp_rate, demod_rate, sca_demod_rate, audio_rate)
196
197         if options.gain is None:
198             # if no gain was specified, use the mid-point in dB
199             g = self.subdev.gain_range()
200             options.gain = float(g[0]+g[1])/2
201
202         if options.volume is None:
203             g = self.volume_range()
204             options.volume = float(g[0]+g[1])/2
205
206         if abs(options.freq) < 1e6:
207             options.freq *= 1e6
208
209         # set initial values
210
211         self.set_gain(options.gain)
212         self.set_vol(options.volume)
213         if not(self.set_freq(options.freq)):
214             self._set_status_msg("Failed to set initial frequency")
215         self.set_sca_freq(67000)  # A common SCA Frequency
216
217
218     def _set_status_msg(self, msg, which=0):
219         self.frame.GetStatusBar().SetStatusText(msg, which)
220
221
222     def _build_gui(self, vbox, usrp_rate, demod_rate, sca_demod_rate, audio_rate):
223
224         def _form_set_freq(kv):
225             return self.set_freq(kv['freq'])
226
227         def _form_set_sca_freq(kv):
228             return self.set_sca_freq(kv['sca_freq'])
229
230         if 1:
231             self.src_fft = fftsink.fft_sink_c (self, self.panel, title="Data from USRP",
232                                                fft_size=512, sample_rate=usrp_rate)
233             self.connect (self.u, self.src_fft)
234             vbox.Add (self.src_fft.win, 4, wx.EXPAND)
235
236         if 1:
237             post_demod_fft = fftsink.fft_sink_f (self, self.panel, title="Post FM Demod",
238                                                 fft_size=2048, sample_rate=demod_rate,
239                                                 y_per_div=10, ref_level=0)
240             self.connect (self.fm_demod, post_demod_fft)
241             vbox.Add (post_demod_fft.win, 4, wx.EXPAND)
242
243         if 0:
244             post_demod_sca_fft = fftsink.fft_sink_f (self, self.panel, title="Post SCA Demod",
245                                                 fft_size=1024, sample_rate=sca_demod_rate,
246                                                 y_per_div=10, ref_level=0)
247             self.connect (self.fm_demod_sca, post_demod_sca_fft)
248             vbox.Add (post_demod_sca_fft.win, 4, wx.EXPAND)
249
250         if 0:
251             post_deemph_fft = fftsink.fft_sink_f (self, self.panel, title="Post SCA Deemph",
252                                                   fft_size=512, sample_rate=audio_rate,
253                                                   y_per_div=10, ref_level=-20)
254             self.connect (self.deemph, post_deemph_fft)
255             vbox.Add (post_deemph_fft.win, 4, wx.EXPAND)
256
257
258         # control area form at bottom
259         self.myform = myform = form.form()
260
261         hbox = wx.BoxSizer(wx.HORIZONTAL)
262         hbox.Add((5,0), 0)
263         myform['freq'] = form.float_field(
264             parent=self.panel, sizer=hbox, label="Freq", weight=1,
265             callback=myform.check_input_and_call(_form_set_freq, self._set_status_msg))
266
267         hbox.Add((5,0), 0)
268         myform['freq_slider'] = \
269             form.quantized_slider_field(parent=self.panel, sizer=hbox, weight=3,
270                                         range=(87.9e6, 108.1e6, 0.1e6),
271                                         callback=self.set_freq)
272         hbox.Add((5,0), 0)
273         vbox.Add(hbox, 0, wx.EXPAND)
274
275         hbox = wx.BoxSizer(wx.HORIZONTAL)
276         hbox.Add((5,0), 0)
277         myform['sca_freq'] = form.float_field(
278             parent=self.panel, sizer=hbox, label="SCA", weight=1,
279             callback=myform.check_input_and_call(_form_set_sca_freq, self._set_status_msg))
280
281         hbox.Add((5,0), 0)
282         myform['sca_freq_slider'] = \
283             form.quantized_slider_field(parent=self.panel, sizer=hbox, weight=3,
284                                         range=(38e3, 100e3, 1.0e3),
285                                         callback=self.set_sca_freq)
286         hbox.Add((5,0), 0)
287         vbox.Add(hbox, 0, wx.EXPAND)
288
289         hbox = wx.BoxSizer(wx.HORIZONTAL)
290         hbox.Add((5,0), 0)
291
292         myform['volume'] = \
293             form.quantized_slider_field(parent=self.panel, sizer=hbox, label="Volume",
294                                         weight=3, range=self.volume_range(),
295                                         callback=self.set_vol)
296         hbox.Add((5,0), 1)
297
298         myform['gain'] = \
299             form.quantized_slider_field(parent=self.panel, sizer=hbox, label="Gain",
300                                         weight=3, range=self.subdev.gain_range(),
301                                         callback=self.set_gain)
302         hbox.Add((5,0), 0)
303         vbox.Add(hbox, 0, wx.EXPAND)
304
305         try:
306             self.knob = powermate.powermate(self.frame)
307             self.rot = 0
308             powermate.EVT_POWERMATE_ROTATE (self.frame, self.on_rotate)
309             powermate.EVT_POWERMATE_BUTTON (self.frame, self.on_button)
310         except:
311             print "FYI: No Powermate or Contour Knob found"
312
313
314     def on_rotate (self, event):
315         self.rot += event.delta
316         if (self.state == "FREQ"):
317             if self.rot >= 3:
318                 self.set_freq(self.freq + .1e6)
319                 self.rot -= 3
320             elif self.rot <=-3:
321                 self.set_freq(self.freq - .1e6)
322                 self.rot += 3
323         else:
324             step = self.volume_range()[2]
325             if self.rot >= 3:
326                 self.set_vol(self.vol + step)
327                 self.rot -= 3
328             elif self.rot <=-3:
329                 self.set_vol(self.vol - step)
330                 self.rot += 3
331
332     def on_button (self, event):
333         if event.value == 0:        # button up
334             return
335         self.rot = 0
336         if self.state == "FREQ":
337             self.state = "VOL"
338         else:
339             self.state = "FREQ"
340         self.update_status_bar ()
341
342
343     def set_vol (self, vol):
344         g = self.volume_range()
345         self.vol = max(g[0], min(g[1], vol))
346         self.volume_control.set_k(10**(self.vol/10))
347         self.myform['volume'].set_value(self.vol)
348         self.update_status_bar ()
349
350     def set_freq(self, target_freq):
351         """
352         Set the center frequency we're interested in.
353
354         @param target_freq: frequency in Hz
355         @rypte: bool
356
357         Tuning is a two step process.  First we ask the front-end to
358         tune as close to the desired frequency as it can.  Then we use
359         the result of that operation and our target_frequency to
360         determine the value for the digital down converter.
361         """
362         r = usrp.tune(self.u, 0, self.subdev, target_freq)
363
364         if r:
365             self.freq = target_freq
366             self.myform['freq'].set_value(target_freq)         # update displayed value
367             self.myform['freq_slider'].set_value(target_freq)  # update displayed value
368             self.update_status_bar()
369             self._set_status_msg("OK", 0)
370             return True
371
372         self._set_status_msg("Failed", 0)
373         return False
374
375     def set_sca_freq(self, target_sca_freq):
376
377         self.ddc.set_center_freq(-target_sca_freq)
378         self.myform['sca_freq'].set_value(target_sca_freq)         # update displayed value
379         self.myform['sca_freq_slider'].set_value(target_sca_freq)  # update displayed value
380         self.update_status_bar()
381         self._set_status_msg("OK", 0)
382         return True
383
384     def set_gain(self, gain):
385         self.myform['gain'].set_value(gain)     # update displayed value
386         self.subdev.set_gain(gain)
387
388     def update_status_bar (self):
389         msg = "Volume:%r  Setting:%s" % (self.vol, self.state)
390         self._set_status_msg(msg, 1)
391         self.src_fft.set_baseband_freq(self.freq)
392
393     def volume_range(self):
394         return (-20.0, 0.0, 0.5)
395
396
397 if __name__ == '__main__':
398     app = stdgui.stdapp (wfm_rx_sca_graph, "USRP WFM SCA RX")
399     app.MainLoop ()