In [1]:
from rtlsdr import RtlSdr
In [2]:
import numpy as np  
import scipy.signal as signal
In [3]:
import matplotlib.pyplot as plt
In [4]:
F_rcv = 100.6e6  # 100.6 MHz = Radio City
F_offset = 250e3  # 250 kHz offset to avoid noise
SAMPLE_RATE = 1140000
SECONDS = 10
In [5]:
F_center = F_rcv - F_offset  # Tune to (desired freq) - offset
N_SAMPLES = 8192000 #SECONDS * SAMPLE_RATE

Open RTL-SDR

In [6]:
sdr = RtlSdr()
sdr.sample_rate = SAMPLE_RATE
sdr.center_freq = F_center
sdr.gain = "auto"

Capture N_SAMPLES samples and close the device

In [28]:
%%time
raw_samples = sdr.read_samples(N_SAMPLES)
sdr.close()
CPU times: user 7 µs, sys: 2 µs, total: 9 µs
Wall time: 12.9 µs

Convert to complex number array

In [8]:
samples = np.array(raw_samples).astype("complex64")

Raw data

In [9]:
plt.specgram(samples, NFFT=2048, Fs=SAMPLE_RATE)
plt.show()

Undo the frequency shift (center on F_rcv)

In [10]:
shift_func = np.exp(-1.0j*2.0*np.pi* F_offset/SAMPLE_RATE*np.arange(len(samples)))  
In [11]:
centered = samples * shift_func
In [12]:
plt.specgram(centered, NFFT=2048, Fs=SAMPLE_RATE)
plt.show()

Crop signal to width BANDWIDTH

In [13]:
BANDWIDTH = 200e3  # Wideband FM has a bandwidth of 200 kHz
In [14]:
decimate_rate = int(SAMPLE_RATE / BANDWIDTH)
focused = signal.decimate(centered, decimate_rate)
decimated_sample_rate = SAMPLE_RATE / decimate_rate
In [15]:
plt.specgram(focused, NFFT=2048, Fs=decimated_sample_rate)
plt.show()

And a polar plot, because why not?

In [16]:
N = 5000
plt.scatter(np.real(focused[0:N]), np.imag(focused[0:N]), alpha=0.05)  
plt.show()