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