{"id":13582704,"url":"https://github.com/mryndzionek/composable-sdr","last_synced_at":"2025-04-21T15:31:12.142Z","repository":{"id":74153653,"uuid":"254839757","full_name":"mryndzionek/composable-sdr","owner":"mryndzionek","description":"SDR DSP utilities embedded in Haskell","archived":false,"fork":false,"pushed_at":"2024-10-23T20:13:56.000Z","size":17178,"stargazers_count":36,"open_issues_count":0,"forks_count":6,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-01T14:14:34.490Z","etag":null,"topics":["dsp","haskell","liquid-dsp","liquid-dsp-library","rtlsdr","rtlsdr-dongle","sdr","soapysdr","software-defined-radio","streamly"],"latest_commit_sha":null,"homepage":null,"language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mryndzionek.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-11T10:03:53.000Z","updated_at":"2024-10-23T20:14:00.000Z","dependencies_parsed_at":null,"dependency_job_id":"0a9dae8a-4833-4424-9368-3db95283dfe0","html_url":"https://github.com/mryndzionek/composable-sdr","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mryndzionek%2Fcomposable-sdr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mryndzionek%2Fcomposable-sdr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mryndzionek%2Fcomposable-sdr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mryndzionek%2Fcomposable-sdr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mryndzionek","download_url":"https://codeload.github.com/mryndzionek/composable-sdr/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250080575,"owners_count":21371532,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsp","haskell","liquid-dsp","liquid-dsp-library","rtlsdr","rtlsdr-dongle","sdr","soapysdr","software-defined-radio","streamly"],"created_at":"2024-08-01T15:02:57.268Z","updated_at":"2025-04-21T15:31:09.642Z","avatar_url":"https://github.com/mryndzionek.png","language":"Haskell","funding_links":[],"categories":["Haskell"],"sub_categories":[],"readme":"# composable-sdr\n\nDSP processing blocks aimed at SDR, embedded in Haskell.\n\n![logo](soapy-sdr.png)\n\n![build](https://github.com/mryndzionek/composable-sdr/workflows/build/badge.svg)\n\n## Introduction\n\nThis repo is aimed at exploring the usefulness of data flow programming for\nSDR/DSP processing. It leverages [SoapySDR](https://github.com/pothosware/SoapySDR)\nfor data sources and [liquid-dsp](https://github.com/jgaeddert/liquid-dsp) for radio\nDSP. All those low-level C/C++ libraries are fine for 'programming in the small', but when\n'programming in the large' code gets ugly really fast. And here is where Haskell comes in.\nThe idea is to use C/C++ interop to 'lift' low-level APIs to streams and folds in [Streamly](https://hackage.haskell.org/package/streamly).\nThe hope is to create a framework in which efficient DSP is possible and without sacrificing\ncode quality even for complex signal processing flows. In other words this will be a 'vehicle'\nto connect [liquid-dsp](https://github.com/jgaeddert/liquid-dsp) functions into more complex and powerful topologies\nwhile preserving desired non-functional requirements.\n\nThis repo will stay fairly low-level. The applications built using provided processing blocks\nshould be lean and mean, so that it's possible to deploy them on even not-so-powerful, embedded,\nheadless systems. All the UI interaction stuff can be always done via sockets and this\nis probably one of the best designs for such things.\n\nAs of today there is only one fairly minimalistic application implemented, but I\nthink the approach already shows its benefits. Writing an application with just one set of\nfunctionalities offered by `soapy-sdr`, in an imperative language, would be an accomplishment in itself.\nMore details below.\n\n## Dependencies\n\n - [SoapySDR](https://github.com/pothosware/SoapySDR)\n - SoapySDR module(s) like [SoapyRTLSDR](https://github.com/pothosware/SoapyRTLSDR)\n - [liquid-dsp](https://github.com/jgaeddert/liquid-dsp)\n\nCabal 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`\nis needed. There is also a `stack.yaml` file available, but might not always be up-to-date, as I'm mostly using\nthe Cabal v2 setup.\n\n## Libraries and modules\n\n### ComposableSDR\n\nAll the C-interop and Streamly streams and folds - one file for now.\n\n## Tools and applications\n\n### soapy-sdr\n\nI/Q recorder and processor using SoapySDR as backend.\n\n#### :warning: All the below screenshots are from other applications. This is purely CLI application.\n\n![soapy-sdr](images/soapy-sdr.png)\n\nBlue arrows are choices in signal flow. First choice is the data source:\nSoapySDR compatible SDR receiver, or a CF32/WAV file.\nNext choice is usage of the PFB channelizer.\nIf it's enabled, then the output signals can be written\nto separate files, or mixed together and written into one file.\nThe raw/modulated signal output file format is CF32.\nFiles can be opened in [inspectrum](https://github.com/miek/inspectrum).\n\n```\nUsage: soapy-sdr [--filename NAME [--chunksize INT] | [--devname NAME] \n                   [-f|--frequency DOUBLE] [-g|--gain DOUBLE]] \n                 [-s|--samplerate DOUBLE] [--offset DOUBLE] \n                 [-b|--bandwidth DOUBLE] [-n|--numsamples INT] \n                 [-o|--output FILENAME] [--demod ARG] [-a|--agc DOUBLE] \n                 [-c|--channels INT] [-m|--mix]\n  Process samples from an SDR retrieved via SoapySDR\n\nAvailable options:\n  --filename NAME          Input (CF32) file name\n  --chunksize INT          Chunk size ins CF32 sample (default: 1024)\n  --devname NAME           Soapy device/driver name (default: \"rtlsdr\")\n  -f,--frequency DOUBLE    Rx frequency in Hz (default: 1.0e8)\n  -g,--gain DOUBLE         SDR gain level (0 = auto) (default: 0.0)\n  -s,--samplerate DOUBLE   Sample rate in Hz (default: 2560000.0)\n  --offset DOUBLE          Offset frequency in Hz (default: 0.0)\n  -b,--bandwidth DOUBLE    Desired output bandwidth in [Hz] (0 = samplerate = no\n                           resampling/decimation) (default: 0.0)\n  -n,--numsamples INT      Number of samples to capture (default: 1024)\n  -o,--output FILENAME     Output file(s) name (without\n                           extension) (default: \"output\")\n  --demod ARG              Demodulation type (default: DeNo)\n  -a,--agc DOUBLE          Enable AGC with squelch threshold in [dB] (0 = no\n                           AGC) (default: 0.0)\n  -c,--channels INT        Number of channels to split the signal\n                           into (default: 1)\n  -m,--mix                 Instead of outputting separate file for each channel,\n                           mix them into one\n  -h,--help                Show this help text\n\n```\n\nThere is an experimental [AppImage](https://appimage.org/) deployment workflow producing\nself-contained binaries from every commit pushed to `build` branch. SoapyRTLSDR module\nis bundled inside the image. Other modules should be detectable from the host system\nas long they reside in standard path (`/usr/local/lib/SoapySDR/modulesx.y`, etc.) and\nare compiled with compatible versions. This however wasn't tested much and long-term\nthe safest solution might be bundling all the (tested/supported) SoapySDR modules.\n\nSome captures from ISM 433MHz\n\n![spectrum1](images/inspectrum1.png)\n![spectrum2](images/inspectrum2.png)\n\nLoRa on 868MHz\n\n![spectrum3](images/inspectrum3.png)\n\n#### Example 1\n\nLet's first check using [CubicSDR](https://cubicsdr.com/) if there are any signals within FM radio band (88-108MHz).\n\n![ex1_1](images/ex1_1.png)\n\nWe see a station on 92MHz. Bandwidth of the signal seems to be around 200kHz. Let's record some IQ samples\n(2 million sample = 10s of recording, DeNo means no demodulation - output will be CF32 IQ sample file):\n\n```sh\ncabal v2-run -- soapy-sdr -n 2000000 -f 92.0e6 -b 200000 --demod \"DeNo\"\n```\n\nLet's now inspect the output file (output.cf32) in [inspectrum](https://github.com/miek/inspectrum).\n\n![ex1_2](images/ex1_2.png)\n\nNow let's record a wideband WAV file with FM demodulated signal:\n\n```sh\ncabal v2-run -- soapy-sdr -n 2000000 -f 92.0e6 -b 200000 --demod \"DeNBFM 0.6 WAV\"\n```\n\nSample rate of this file is 200kHz. Didn't know libsndfile can pull this off :smiley:. On a spectrogram in\n[Audacity](https://www.audacityteam.org/) we can clearly see the mono audio below 15kHz, 19kHz stereo pilot, stereo audio\nbetween 23kHz and 53kHz and RDBS around 57kHz.\n\n![ex1_3](images/ex1_3.png)\n\nThe same in baudline:\n\n![ex1_4](images/ex1_4.png)\n\nAlright, let's now do proper wide band FM (mono) demodulation with de-emphasis, resampled rate of 192k and output decimation of 4,\nto get 48kHz output WAV file:\n\n```sh\ncabal v2-run -- soapy-sdr -n 2000000 -f 92.0e6 -b 192000 --demod \"DeWBFM 4 WAV\"\n```\n\nThere is also experimental stereo FM decoder:\n\n```sh\ncabal v2-run -- soapy-sdr -n 2000000 -f 92.0e6 -b 192000 --demod \"DeFMS 4 WAV\"\n```\n\nIt's possible to 'play live' running below commands in a separate terminal:\n\n```sh\nrm output*; mkfifo output.au \u0026\u0026 play output.au\n```\nand then starting `soapy-sdr`, but with AU audio format set.\n\n#### Example 2\n\nTo run as a [PMR446](https://en.wikipedia.org/wiki/PMR446) scanner:\n\n```sh\ncabal v2-run -- soapy-sdr -n 2000000 -f 446.1e6 -b 200000 -c 16 -s 1.0e6 \\\n--demod \"DeNBFM 0.3 WAV\" -g 40 -a -16\n```\n\nThis will output 16 WAV files, each for one PMR channel. To merge all the files into one `-m` flag can be used:\nThere is also AGC with squelch (`-a` option), but needs more testing and adding auto mode.\n\n#### Example 3\n\nJust a general demonstration of precision and efficiency. We're switching the SDR to 3.2MSPS.\nThen resample the signal to 1.6MSPS and channelize to 20 channels, writing to 20 separate files.\nWe request 16M sample (after resampling), so around 10s of recording:\n\n```sh\ntime cabal v2-run -- soapy-sdr -n 16000000 -f 433.9e6 -s 3.2e6 -b 1.6e6 \\\n--demod \"DeNo\" -g 35 -a -50 -c 20\n```\n\nBelow is a GIF showing how the files are written and CPU utilization. No samples are lost and each file\nends up 6400000 bytes long. One CF32 sample is 8 bytes, so at 3.2MSPS we're capturing around 24MB/s.\nThen processing it and saving around 122MB in 10 seconds.\n\n![ex1_5](images/ex1_5.gif)\n\n#### Example 4\n\nCapturing raw CF32 IQ samples file and demodulating offline.\n\nCapturing a slice of FM band at maximum samplerate (3.2MSPS) of RTL-SDR to a file `input.cf32`.\n192MS is about a minute of recording and 24MB/s, so overall 1.43GB of data:\n\n```sh\ncabal v2-run -- soapy-sdr -n 192000000 -f 91.0e6 -s 3.2e6 --demod \"DeNo\" -o input\n```\n\nI've tuned to 91MHz and we now that at 92MHz there a station. Let's mix down and resample\n192kHz wide slice of this part of the spectrum:\n\n```sh\ncabal v2-run -- soapy-sdr --filename input.cf32 -n 192000000 -s 3200000 \\\n--offset 1.0e6 -b 192000 --demod \"DeNo\"\n```\n\nThen we can demodulate the extracted slice of spectrum:\n\n```sh\ncabal v2-run -- soapy-sdr --filename output.cf32 -n 192000000 -s 192000 --demod \"DeWBFM 4 WAV\"\n```\n\n#### Example 5\n\nDemodulating signal from a sensor transmitting on 433MHz ISM band (software AGC with squelch enabled):\n\n```sh\ncabal v2-run -- soapy-sdr -n 200000 -f 434.388e6 -b 20000 -s 1.0e6 \\\n--demod \"DeNBFM 0.3 WAV\" -g 30 -a -50\n```\n\n![spectrum4](images/inspectrum4.png)\n\n#### Example 6\n\nDecoding FSK data from a helicopter, similarly to [this](http://www.windytan.com/2014/02/mystery-signal-from-helicopter.html) and [this](https://github.com/proto17/HelicopterDemod/wiki).\nFirst let's download the audio containing FSK signal from [youtube](https://www.youtube.com/watch?v=2MprHxarmOI) using [youtube-dl](https://youtube-dl.org/):\n\n```sh\nyoutube-dl -x --audio-format wav https://www.youtube.com/watch?v=2MprHxarmOI\n```\n\nThe file has two channels, so convert it to mono using [Audacity](https://www.audacityteam.org/).\nOpen the WAV in Auadacity and then `Tracks -\u003e Mix -\u003e 'Mix Stereo down to Mono'`. Finally save the converted file and rename it to `helicopter.wav`.\nConvert the WAV file to IQ (.cf32) data file for analysis using [inspectrum](https://github.com/miek/inspectrum):\n\n```sh\ncabal v2-run -- soapy-sdr --filename helicopter.wav -n 19200000000 -s 24000 --demod \"DeNo\"\n```\n\nIn inspectrum we see the signal between 1.2kHz and 2.4kHz:\n\n![ex6_1](images/ex6_1.png)\n\nLet's now try filter out and FM demodulate the signal:\n\n```sh\ncabal v2-run -- soapy-sdr --filename helicopter.wav -n 20000000000 -s 24000 \\\n--offset 1.8e3 -b 6.0e3 --demod \"DeNBFM 0.6 WAV\"\n```\n\nIn the resulting WAV file (`output.wav`) we should see something resembling a square wave:\n\n![ex6_2](images/ex6_2.png)\n\nNot perfect, but we can try to FM demodulate with with timing recovery to get something workable:\n\n```sh\ncabal v2-run -- soapy-sdr --filename helicopter.wav -n 20000000000 -s 24000 \\\n--offset 1.8e3 -b 4.8e3 --demod \"DeNBFMSync 4\"\n```\n\nThe `4` signifies 4 samples per symbol (The bitrate is 1200bit/s, we resampled to 4800Hz, so 4800/1200=4).\nThis will result in `output.f32` file with the demodulated signal and symbols synchronized (2 samples per symbol).\n\nWithout symbol synchronizer:\n\n![ex6_5](images/ex6_5.png)\n\nWith symbol synchronizer:\n\n![ex6_6](images/ex6_6.png)\n\n\nFurther processing requires some trial and error.\nMore info can be found [here](https://github.com/proto17/HelicopterDemod/wiki).\nThere is a simple decoding app in this [repo](apps/HeliDecode.hs) that works slightly differently.\nWhen run like this:\n\n```sh\ncabal v2-run -- helidecode output.f32\n```\n\nthe app will output a KML (`output.kml`) file that can be visualized using [GpsPrune](https://activityworkshop.net/software/gpsprune/):\n\n![ex6_3](images/ex6_3.png)\n\n![ex6_4](images/ex6_4.png)\n\n\n## TODO\n  - [ ] eliminate the need for mandatory `-n,--numsamples` argument\n  - [ ] add live playback via PulseAudio\n  - [ ] add RF protocol decoders\n  - [ ] profile flows and introduce concurrency modifiers (`aheadly`, etc.)\n  - [ ] Template Haskell boilerplate code generator for Liquid-DSP blocks\n  - [ ] 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)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmryndzionek%2Fcomposable-sdr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmryndzionek%2Fcomposable-sdr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmryndzionek%2Fcomposable-sdr/lists"}