summaryrefslogtreecommitdiff
path: root/numpy_fm_demod.py
blob: f478105dfdc55eb1060d3c03fa9aa42082b5cf6f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import sys
import numpy as np
import scipy.signal as signal

from rtlsdr import RtlSdr


class NumpyFmDemod():
    '''
    Numpy-based FM signal demodulation
    Based on the great tutorial by Fraida Fund
    https://witestlab.poly.edu/blog/capture-and-decode-fm-radio/
    '''
    def __init__(self, frequency, sample_rate=1800000, sample_count=8192000, dc_offset=250000):
        self.freq = int(frequency)
        self.sample_rate = sample_rate
        self.sample_count = sample_count
        self.dc_offset = dc_offset

    def capture_samples(self):
        sdr = RtlSdr()
        sdr.sample_rate = self.sample_rate
        sdr.center_freq = self.freq - self.dc_offset
        sdr.gain = 'auto'
        self.samples = sdr.read_samples(self.sample_count)
        self.samples_to_np()
        sdr.close()

    def load_samples(self, filename):
        self.samples = np.load(filename)

    def dump_samples(self, filename):
        np.save(filename, self.samples)

    def decimate(self, rate):
        '''
        Utility function to decimate signal by a given rate
        '''
        self.samples = signal.decimate(self.samples, rate)
        self.sample_rate /= rate

    def samples_to_np(self):
        '''
        Convert samples to a complex numpy array
        '''
        self.samples = np.array(self.samples).astype('complex64')

    def mix_down_dc_offset(self):
        '''
        Mix the samples back down to account for DC offset
        '''
        fc1 = np.exp(-1.0j * 2.0 * np.pi * self.dc_offset / self.sample_rate * np.arange(len(self.samples)))
        self.samples *= fc1

    def lowpass_filter(self):
        '''
        Apply low-pass filter to catch only the target frequency
        '''
        BANDWIDTH = 200000  # wideband FM signal is always 200kHz
        decimation_rate = int(self.sample_rate / BANDWIDTH)
        self.decimate(decimation_rate)

    def polar_discriminator(self):
        '''
        Apply a polar discriminator to demodulate the FM signal
        '''
        self.samples = np.angle(self.samples[1:] * np.conj(self.samples[:-1]))

    def de_emphasis_filter(self):
        '''
        Apply a de-emphasis filter
        Still need to figure out exactly what's going on here
        '''
        d = self.sample_rate * 75e-6
        x = np.exp(-1/d)
        b, a = [1-x], [1,-x]
        self.samples = signal.lfilter(b, a, self.samples)

    def mono_decimate(self):
        '''
        Decimate the signal to catch the mono transmission
        '''
        audio_freq = 44100.0
        decimation_rate = int(self.sample_rate / audio_freq)
        self.decimate(decimation_rate)

    def scale_volume(self):
        '''
        Scale samples to adjust volume
        '''
        self.samples *= 10000 / np.max(np.abs(self.samples))

    def output_file(self, filename, astype='int16'):
        self.samples.astype(astype).tofile(filename)

    def demod(self):
        self.mix_down_dc_offset()
        self.lowpass_filter()
        self.polar_discriminator()
        self.de_emphasis_filter()
        self.mono_decimate()
        self.scale_volume()

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print('Usage: numpy_fm_demod.py <command> [arg]\n')
        exit()
    else:
        cmd = sys.argv[1]
        try:
            arg = sys.argv[2]
        except:
            arg = None

    nfd = NumpyFmDemod(frequency=91.8e6)
    if cmd == 'load':
        nfd.load_samples(arg)
        nfd.demod()
        nfd.output_file(f'wbfm-mono-{nfd.sample_rate}.raw', astype='int16')
    elif cmd == 'capture':
        nfd.capture_samples()
        nfd.demod()
        nfd.output_file(f'wbfm-mono-{nfd.sample_rate}.raw', astype='int16')
    elif cmd == 'dump':
        nfd.capture_samples()
        nfd.dump_samples(arg)