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