DSP processing blocks aimed at SDR, embedded in Haskell.
This repo is aimed at exploring the usefulness of data flow programming for SDR/DSP processing. It leverages SoapySDR for data sources and liquid-dsp for radio DSP. All those low-level C/C++ libraries are fine for 'programming in the small', but when 'programming in the large' code gets ugly really fast. And here is where Haskell comes in. The idea is to use C/C++ interop to 'lift' low-level APIs to streams and folds in Streamly. The hope is to create a framework in which efficient DSP is possible and without sacrificing code quality even for complex signal processing flows. In other words this will be a 'vehicle' to connect liquid-dsp functions into more complex and powerful topologies while preserving desired non-functional requirements.
This repo will stay fairly low-level. The applications built using provided processing blocks should be lean and mean, so that it's possible to deploy them on even not-so-powerful, embedded, headless systems. All the UI interaction stuff can be always done via sockets and this is probably one of the best designs for such things.
As of today there is only one fairly minimalistic application implemented, but I
think the approach already shows its benefits. Writing an application with just one set of
functionalities offered by soapy-sdr
, in an imperative language, would be an accomplishment in itself.
More details below.
- SoapySDR
- SoapySDR module(s) like SoapyRTLSDR
- liquid-dsp
Cabal v2 project requires ghc-8.6.5, so before first cabal v2-build
something like cabal v2-configure -w /opt/ghc/8.6.5/bin/ghc-8.6.5
is needed. There is also a stack.yaml
file available, but might not always be up-to-date, as I'm mostly using
the Cabal v2 setup.
All the C-interop and Streamly streams and folds - one file for now.
I/Q recorder and processor using SoapySDR as backend.
Blue arrows are choices in signal flow. First choice is the data source: SoapySDR compatible SDR receiver, or a CF32/WAV file. Next choice is usage of the PFB channelizer. If it's enabled, then the output signals can be written to separate files, or mixed together and written into one file. The raw/modulated signal output file format is CF32. Files can be opened in inspectrum.
Usage: soapy-sdr [--filename NAME [--chunksize INT] | [--devname NAME]
[-f|--frequency DOUBLE] [-g|--gain DOUBLE]]
[-s|--samplerate DOUBLE] [--offset DOUBLE]
[-b|--bandwidth DOUBLE] [-n|--numsamples INT]
[-o|--output FILENAME] [--demod ARG] [-a|--agc DOUBLE]
[-c|--channels INT] [-m|--mix]
Process samples from an SDR retrieved via SoapySDR
Available options:
--filename NAME Input (CF32) file name
--chunksize INT Chunk size ins CF32 sample (default: 1024)
--devname NAME Soapy device/driver name (default: "rtlsdr")
-f,--frequency DOUBLE Rx frequency in Hz (default: 1.0e8)
-g,--gain DOUBLE SDR gain level (0 = auto) (default: 0.0)
-s,--samplerate DOUBLE Sample rate in Hz (default: 2560000.0)
--offset DOUBLE Offset frequency in Hz (default: 0.0)
-b,--bandwidth DOUBLE Desired output bandwidth in [Hz] (0 = samplerate = no
resampling/decimation) (default: 0.0)
-n,--numsamples INT Number of samples to capture (default: 1024)
-o,--output FILENAME Output file(s) name (without
extension) (default: "output")
--demod ARG Demodulation type (default: DeNo)
-a,--agc DOUBLE Enable AGC with squelch threshold in [dB] (0 = no
AGC) (default: 0.0)
-c,--channels INT Number of channels to split the signal
into (default: 1)
-m,--mix Instead of outputting separate file for each channel,
mix them into one
-h,--help Show this help text
There is an experimental AppImage deployment workflow producing
self-contained binaries from every commit pushed to build
branch. SoapyRTLSDR module
is bundled inside the image. Other modules should be detectable from the host system
as long they reside in standard path (/usr/local/lib/SoapySDR/modulesx.y
, etc.) and
are compiled with compatible versions. This however wasn't tested much and long-term
the safest solution might be bundling all the (tested/supported) SoapySDR modules.
Some captures from ISM 433MHz
LoRa on 868MHz
Let's first check using CubicSDR if there are any signals within FM radio band (88-108MHz).
We see a station on 92MHz. Bandwidth of the signal seems to be around 200kHz. Let's record some IQ samples (2 million sample = 10s of recording, DeNo means no demodulation - output will be CF32 IQ sample file):
cabal v2-run -- soapy-sdr -n 2000000 -f 92.0e6 -b 200000 --demod "DeNo"
Let's now inspect the output file (output.cf32) in inspectrum.
Now let's record a wideband WAV file with FM demodulated signal:
cabal v2-run -- soapy-sdr -n 2000000 -f 92.0e6 -b 200000 --demod "DeNBFM 0.6 WAV"
Sample rate of this file is 200kHz. Didn't know libsndfile can pull this off 😃. On a spectrogram in Audacity we can clearly see the mono audio below 15kHz, 19kHz stereo pilot, stereo audio between 23kHz and 53kHz and RDBS around 57kHz.
The same in baudline:
Alright, let's now do proper wide band FM (mono) demodulation with de-emphasis, resampled rate of 192k and output decimation of 4, to get 48kHz output WAV file:
cabal v2-run -- soapy-sdr -n 2000000 -f 92.0e6 -b 192000 --demod "DeWBFM 4 WAV"
There is also experimental stereo FM decoder:
cabal v2-run -- soapy-sdr -n 2000000 -f 92.0e6 -b 192000 --demod "DeFMS 4 WAV"
It's possible to 'play live' running below commands in a separate terminal:
rm output*; mkfifo output.au && play output.au
and then starting soapy-sdr
, but with AU audio format set.
To run as a PMR446 scanner:
cabal v2-run -- soapy-sdr -n 2000000 -f 446.1e6 -b 200000 -c 16 -s 1.0e6 \
--demod "DeNBFM 0.3 WAV" -g 40 -a -16
This will output 16 WAV files, each for one PMR channel. To merge all the files into one -m
flag can be used:
There is also AGC with squelch (-a
option), but needs more testing and adding auto mode.
Just a general demonstration of precision and efficiency. We're switching the SDR to 3.2MSPS. Then resample the signal to 1.6MSPS and channelize to 20 channels, writing to 20 separate files. We request 16M sample (after resampling), so around 10s of recording:
time cabal v2-run -- soapy-sdr -n 16000000 -f 433.9e6 -s 3.2e6 -b 1.6e6 \
--demod "DeNo" -g 35 -a -50 -c 20
Below is a GIF showing how the files are written and CPU utilization. No samples are lost and each file ends up 6400000 bytes long. One CF32 sample is 8 bytes, so at 3.2MSPS we're capturing around 24MB/s. Then processing it and saving around 122MB in 10 seconds.
Capturing raw CF32 IQ samples file and demodulating offline.
Capturing a slice of FM band at maximum samplerate (3.2MSPS) of RTL-SDR to a file input.cf32
.
192MS is about a minute of recording and 24MB/s, so overall 1.43GB of data:
cabal v2-run -- soapy-sdr -n 192000000 -f 91.0e6 -s 3.2e6 --demod "DeNo" -o input
I've tuned to 91MHz and we now that at 92MHz there a station. Let's mix down and resample 192kHz wide slice of this part of the spectrum:
cabal v2-run -- soapy-sdr --filename input.cf32 -n 192000000 -s 3200000 \
--offset 1.0e6 -b 192000 --demod "DeNo"
Then we can demodulate the extracted slice of spectrum:
cabal v2-run -- soapy-sdr --filename output.cf32 -n 192000000 -s 192000 --demod "DeWBFM 4 WAV"
Demodulating signal from a sensor transmitting on 433MHz ISM band (software AGC with squelch enabled):
cabal v2-run -- soapy-sdr -n 200000 -f 434.388e6 -b 20000 -s 1.0e6 \
--demod "DeNBFM 0.3 WAV" -g 30 -a -50
Decoding FSK data from a helicopter, similarly to this and this. First let's download the audio containing FSK signal from youtube using youtube-dl:
youtube-dl -x --audio-format wav https://www.youtube.com/watch?v=2MprHxarmOI
The file has two channels, so convert it to mono using Audacity.
Open the WAV in Auadacity and then Tracks -> Mix -> 'Mix Stereo down to Mono'
. Finally save the converted file and rename it to helicopter.wav
.
Convert the WAV file to IQ (.cf32) data file for analysis using inspectrum:
cabal v2-run -- soapy-sdr --filename helicopter.wav -n 19200000000 -s 24000 --demod "DeNo"
In inspectrum we see the signal between 1.2kHz and 2.4kHz:
Let's now try filter out and FM demodulate the signal:
cabal v2-run -- soapy-sdr --filename helicopter.wav -n 20000000000 -s 24000 \
--offset 1.8e3 -b 6.0e3 --demod "DeNBFM 0.6 WAV"
In the resulting WAV file (output.wav
) we should see something resembling a square wave:
Not perfect, but we can try to FM demodulate with with timing recovery to get something workable:
cabal v2-run -- soapy-sdr --filename helicopter.wav -n 20000000000 -s 24000 \
--offset 1.8e3 -b 4.8e3 --demod "DeNBFMSync 4"
The 4
signifies 4 samples per symbol (The bitrate is 1200bit/s, we resampled to 4800Hz, so 4800/1200=4).
This will result in output.f32
file with the demodulated signal and symbols synchronized (2 samples per symbol).
Without symbol synchronizer:
With symbol synchronizer:
Further processing requires some trial and error. More info can be found here. There is a simple decoding app in this repo that works slightly differently. When run like this:
cabal v2-run -- helidecode output.f32
the app will output a KML (output.kml
) file that can be visualized using GpsPrune:
- eliminate the need for mandatory
-n,--numsamples
argument - add live playback via PulseAudio
- add RF protocol decoders
- profile flows and introduce concurrency modifiers (
aheadly
, etc.) - Template Haskell boilerplate code generator for Liquid-DSP blocks
- add automatic tests (IQ .cf32 files can be read already, so having a set of them for testing different processing configurations would be a good idea)