Now to look at some wide FM in the broadcast band. This should be the same as before, but we need to be more aggressive about filtering out the extra cruft above the pilot tone at 19kHz.
%pylab inline
from gnuradio import blocks
from gnuradio import gr
from gnuradio import uhd
from gnuradio import audio
from gnuradio.filter import firdes
import scipy
import scipy.signal
F_station = int(104.5e6)
Fs=int(1e6)
gain=10
N = int(5e6)
Fc = F_station - 250000 # We capture to go a bit low of the center
# Set up our flow graph
# Cribbing off http://gnuradio.org/notebooks/GNURadioforSimulation.html
#
tb = gr.top_block()
# Set up the USRP for input
usrp_source = uhd.usrp_source(",", uhd.stream_args(cpu_format="fc32", channels=[0]))
usrp_source.set_samp_rate(Fs)
usrp_source.set_center_freq(Fc, 0)
usrp_source.set_gain(gain, 0)
# Let's try to flush out the first bunch of samples
skip_head = blocks.skiphead(gr.sizeof_gr_complex, 1000)
# Limit ourselves to N samples
head = blocks.head(gr.sizeof_gr_complex, N)
# And a sink to dump them into
sink = blocks.vector_sink_c()
tb.connect(usrp_source, skip_head, head, sink) # Can use the handy serial connect method here
tb.run()
tb.stop()
usrp_source.stop()
x = np.array(sink.data())
psd(x, Fs=Fs); None
specgram(xp[0:Fs], Fs=Fs); None
def extract_channel(f_sample, f_center, f_bw, x):
"""Extract a channel of width f_bw at f_center, from f_sample
Returns Fs,y, the new sample rate and an array of complex samples
"""
# Cribbing from the GNURadio xlate, we use their block diagram
my_state = {} # cheating backchannel for debug
# Create a LPF for our target bandwidth
n_taps = 100 # XXX Total random shot in the dark
r_cutoff = float(f_bw)/f_sample
base_lpf = scipy.signal.remez(n_taps, [0, r_cutoff, r_cutoff+(0.5-r_cutoff)/4, 0.5], [1,0])
# Shift this LPF up to our center frequency
fwT0 = 2.0*np.pi*(f_center-f_bw/2)/f_sample
lpf = base_lpf * np.exp(1.0j*fwT0 * np.arange(0, n_taps))
dec_rate = int(f_sample / f_bw)
x_filtered = scipy.signal.lfilter(lpf, 1.0, x)
dec_is = np.arange(0, len(x_filtered), dec_rate)
y = x_filtered[dec_is]
y *= np.exp(-1.0j*fwT0*dec_is)
my_state["n_taps"] = n_taps
my_state["r_cutoff"] = r_cutoff
my_state["lpf"] = lpf
my_state["base_lpf"] = base_lpf
my_state["fwT0"] = fwT0
my_state["dec_rate"] = dec_rate
# my_state["dec_is"] = dec_is
# my_state["x_filtered"] = x_filtered
return f_sample/dec_rate, y, my_state
def decode_quad(x, gain):
xp = x[1:] * np.conj(x[:-1])
retval = gain * np.arctan2(np.imag(xp), np.real(xp))
#lpf = scipy.signal.remez(30, [0.05, 0.25, 0.27, 0.5], [1,0])
#retval = scipy.signal.lfilter(lpf, 1.0, retval)
return retval
Fs_new, y, _ = extract_channel(Fs, 350000, 200000, x)
specgram(y[0:Fs], Fs=Fs_new)
Fs_new
And now, to decode the actual audio from the band-limited FM signal. We can just use decode_quad for this.
w = decode_quad(y, 1.0)
plot(w[0:1000])
figure(figsize=(14,6))
specgram(w[0:30000], Fs=Fs_new); None
We can see the audio down below 20kHz, with a strong pilot tone at 19kHz. We should be using that tone to drive a frequency locking loop of some sort, but that's too much work at the moment.
There's another tone at 2×19kHz (just below 40kHz), which is marking off where stereo information goes. And, if you look closely, you'll see another little row of signal at 3×19kHz (just below 60kHz), which is the RDS data signal. More on that later.
For now, let's low-pass the audio and filter the crap out of it to try and get rid of that pilot tone.
lpf_audio = scipy.signal.remez(120, [0,15000, 16000, Fs_new/2], [1,0], Hz=Fs_new)
Fs_audio = 40000
w_audio = scipy.signal.lfilter(lpf_audio, 1.0, w)
w_audio = w_audio[range(0, len(w_audio), Fs_new/Fs_audio)]
specgram(w_audio, Fs=Fs_audio)
Fs_audio
scale_factor = 5000 / np.max(np.abs(w_audio))
(w_audio*scale_factor).astype("int16").tofile("wbfm.raw")
from subprocess import call
cmd_line = "aplay -r %d -f S16_LE -t raw -c 1 wbfm.raw" % (Fs_audio)
call(cmd_line.split(" "))