Digital signal processing primitives

Files
README.md

dsp #

Digital signal processing building blocks for demodulating satellite signals.

Overview #

dsp is the front end of a software-defined radio receiver: the part between the raw IQ samples of an RTL-SDR dongle and the demodulator. Whatever the signal, that part filters, decimates, fixes the gain and, when debugging, looks at the spectrum. The package has complex arithmetic, FIR and root-raised-cosine filters, a decimator, AGC, an IQ file decoder, an FFT and a short-time Fourier transform. Blocks that process one sample at a time carry their own state, so memory stays bounded however long the capture. demod and rtlsdr use it.

Some background, from Proakis and Salehi (Digital Communications, section 9.2):

  • The transmitter shapes each symbol with a root-raised-cosine (RRC) filter. The receiver runs the same filter (hence "matched filter"), and the two in series give a raised-cosine pulse. That pulse maximises SNR at the sampling instant and is zero at the neighbouring symbols' instants, so there is no inter-symbol interference.
  • The roll-off is the excess bandwidth over the symbol rate; 0.35 means 35%.
  • Decimating by N keeps one sample in N. A low-pass filter runs first, otherwise the high frequencies fold back (alias) into the band.
  • AGC scales samples towards a target amplitude, so later stages see a constant level whatever the received power.

Installation #

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam install dsp

Usage #

The matched filter runs on the in-phase (I) and quadrature (Q) components separately. A Fir.t owns its delay line, so it serves one stream only, and a complex signal needs two filters built from the same taps:

(* Design a root-raised-cosine matched filter, one per component *)
let rrc () =
  Dsp.Fir.root_raised_cosine ~samples_per_symbol:4 ~rolloff:0.35 ~num_taps:41

let rrc_i = rrc ()
let rrc_q = rrc ()

(* Read raw IQ samples and filter them *)
let samples = Dsp.Iq.of_bytes (Bytes.make 16 '\128')

let filtered =
  Array.map
    (fun s ->
      Dsp.Complex.v
        (Dsp.Fir.apply rrc_i s.Dsp.Complex.re)
        (Dsp.Fir.apply rrc_q s.im))
    samples

(* Automatic gain control *)
let agc = Dsp.Agc.v ~target:1.0 ~bandwidth:0.05
let agc', normalized = Dsp.Agc.apply agc (Dsp.Complex.v 0.5 0.3)

(* Decimate by 4 with anti-alias filter *)
let lpf = Dsp.Fir.lowpass ~cutoff:0.25 ~num_taps:31
let dec = Dsp.Decimator.v ~filter:lpf 4
let out = Dsp.Decimator.apply_array dec [| 0.; 0.25; 0.5; 0.25; 0. |]

API #

  • Complex: complex numbers with arithmetic, norm, phase and polar conversion.
  • Fir: FIR filters (v, apply, lowpass, root_raised_cosine, coefficients).
  • Decimator: sample-rate reduction (v, apply, apply_array).
  • Agc: automatic gain control (v, apply, gain).
  • Iq: IQ sample decoding from interleaved unsigned bytes (of_bytes, of_file).
  • Fft: radix-2 FFT and its inverse, spectrum shift and bin frequencies.
  • Stft: a streaming short-time Fourier transform over overlapping windows.

Licence #

ISC License. See LICENSE.md for details.