Add Python functions to demodulate FM and to measure quality of 19 kHz pilot.
This commit is contained in:
parent
a256775f28
commit
f1b4dc55d6
93
pyfm.py
93
pyfm.py
|
@ -6,6 +6,7 @@ import sys
|
||||||
import types
|
import types
|
||||||
import numpy
|
import numpy
|
||||||
import numpy.fft
|
import numpy.fft
|
||||||
|
import scipy.signal
|
||||||
|
|
||||||
|
|
||||||
def readRawSamples(fname):
|
def readRawSamples(fname):
|
||||||
|
@ -51,6 +52,52 @@ def freqShiftIQ(d, freqshift):
|
||||||
return d * w
|
return d * w
|
||||||
|
|
||||||
|
|
||||||
|
def firFilter(d, coeff):
|
||||||
|
"""Apply FIR filter to sample stream."""
|
||||||
|
|
||||||
|
# lazy version
|
||||||
|
def g(d, coeff):
|
||||||
|
prev = None
|
||||||
|
for b in d:
|
||||||
|
if prev is None:
|
||||||
|
yield scipy.signal.lfilter(coeff, 1, b)
|
||||||
|
prev = b
|
||||||
|
else:
|
||||||
|
k = min(len(prev), len(coeff))
|
||||||
|
x = numpy.concatenate((prev[-k:], b))
|
||||||
|
y = scipy.signal.lfilter(coeff, 1, x)
|
||||||
|
yield y[k:]
|
||||||
|
if len(coeff) > len(b):
|
||||||
|
prev = x
|
||||||
|
else:
|
||||||
|
prev = b
|
||||||
|
|
||||||
|
if isinstance(d, types.GeneratorType):
|
||||||
|
return g(d, coeff)
|
||||||
|
else:
|
||||||
|
return scipy.signal.lfilter(coeff, 1, d)
|
||||||
|
|
||||||
|
|
||||||
|
def quadratureDetector(d):
|
||||||
|
"""FM frequency detector based on quadrature demodulation."""
|
||||||
|
|
||||||
|
# lazy version
|
||||||
|
def g(d):
|
||||||
|
prev = None
|
||||||
|
for b in d:
|
||||||
|
if prev is None:
|
||||||
|
yield numpy.angle(b[1:] * b[:-1].conj())
|
||||||
|
else:
|
||||||
|
x = numpy.concatenate((prev[-1:], b[:-1]))
|
||||||
|
yield numpy.angle(b * x.conj())
|
||||||
|
prev = b
|
||||||
|
|
||||||
|
if isinstance(d, types.GeneratorType):
|
||||||
|
return g(d)
|
||||||
|
else:
|
||||||
|
return numpy.angle(d[1:] * d[:-1].conj())
|
||||||
|
|
||||||
|
|
||||||
def spectrum(d, fs=1, nfft=None, sortfreq=False):
|
def spectrum(d, fs=1, nfft=None, sortfreq=False):
|
||||||
"""Calculate Welch-style power spectral density.
|
"""Calculate Welch-style power spectral density.
|
||||||
|
|
||||||
|
@ -185,3 +232,49 @@ def pll(d, centerfreq, bandwidth):
|
||||||
return y, phasei, phaseq, phaseerr, freq, phase
|
return y, phasei, phaseq, phaseerr, freq, phase
|
||||||
|
|
||||||
|
|
||||||
|
def pilotLevel(d, fs, freqshift, bw=150.0e3):
|
||||||
|
"""Calculate level of the 19 kHz pilot vs noise floor in the guard band.
|
||||||
|
|
||||||
|
d :: block of raw I/Q samples or lazy I/Q sample stream
|
||||||
|
fs :: sample frequency in Hz
|
||||||
|
freqshift :: frequency offset in Hz
|
||||||
|
bw :: half-bandwidth of IF signal in Hz
|
||||||
|
|
||||||
|
Return (pilot_power, guard_floor, noise)
|
||||||
|
where pilot_power is the power of the pilot tone in dB
|
||||||
|
guard_floor is the noise floor in the guard band in dB/Hz
|
||||||
|
noise is guard_floor - pilot_power in dB/Hz
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Shift frequency
|
||||||
|
if freqshift != 0:
|
||||||
|
d = freqShiftIQ(d, freqshift / float(fs))
|
||||||
|
|
||||||
|
# Filter
|
||||||
|
b = scipy.signal.firwin(31, 2.0 * bw / fs, window='nuttall')
|
||||||
|
d = firFilter(d, b)
|
||||||
|
|
||||||
|
# Demodulate FM.
|
||||||
|
d = quadratureDetector(d)
|
||||||
|
|
||||||
|
# Power spectral density.
|
||||||
|
f, q = spectrum(d, fs=fs, sortfreq=False)
|
||||||
|
|
||||||
|
# Locate 19 kHz bin.
|
||||||
|
k19 = int(19.0e3 * len(q) / fs)
|
||||||
|
kw = 5 + int(100.0 * len(q) / fs)
|
||||||
|
k19 = k19 - kw + numpy.argmax(q[k19-kw:k19+kw])
|
||||||
|
|
||||||
|
# Calculate pilot power.
|
||||||
|
p19 = numpy.sum(q[k19-1:k19+2]) * fs * 0.75 / len(q)
|
||||||
|
|
||||||
|
# Calculate noise floor in guard band.
|
||||||
|
k17 = int(17.0e3 * len(q) / fs)
|
||||||
|
k18 = int(18.0e3 * len(q) / fs)
|
||||||
|
guard = numpy.mean(q[k17:k18])
|
||||||
|
|
||||||
|
p19db = 10 * numpy.log10(p19)
|
||||||
|
guarddb = 10 * numpy.log10(guard)
|
||||||
|
|
||||||
|
return (p19db, guarddb, guarddb - p19db)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue