Lightweight modular SDR runtime in pure C
PhaseHound is a modular SDR core designed for people who actually care about RF, latency, architecture, and control — not just pretty GUIs.
-
Core daemon (
ph-core)
A broker that manages local "feeds", routes messages, and glues modules together. Think lightweight signal bus for SDR. -
Runtime-loadable add-ons (.so plugins)
Each add-on can publish IQ streams, demodulate, decode, sink audio, log metadata, etc.
Add-ons register feeds dynamically and talk to the core at runtime.
They can be loaded / unloaded without restarting. -
CLI (
ph-cli)
A tiny control tool that speaks the core protocol over a Unix domain socket. You can:- list addons / feeds
- subscribe to live output
- send commands to addons
- load/unload addons
- inspect IQ/PCM ring announcements
The goal: a clean SDR processing pipeline that is:
- headless
- scriptable
- zero bloat
- fast enough for real RF work
- fully dynamic and explicitly wired by the user through feed subscriptions.
Most SDR stacks fall into 2 buckets:
- Giant GUI labs (heavy, slow, not deployable).
- Hardcoded demod chains that can't be composed.
PhaseHound is a middle layer:
- It's not a GUI.
- It's not a monolithic demod.
- It's a signal routing runtime that lets you wire:
RF source → DSP stages → decoders → sinks
…using dynamically loaded plugins.
Example pipeline:
- SoapySDR RF source (
soapy) - Wide FM demodulator (
wfmd) - Audio sink (
audiosink) - Monitoring via CLI
- All talking via shared-memory rings + control feeds.
You extend it by dropping .so files — no rebuilding the core.
+----------------------+
| ph-cli |
| (control / monitor) |
+----------------------+
|
| Unix Domain Socket
v
+------------------+
| ph-core |
| (the broker) |
+------------------+
|
+-----------------------------------+
| |
+--------------+ +---------------+
| soapy.so | | wfmd.so |
| RF source | | WFM demod |
| (IQ ring) | | (PCM ring) |
+--------------+ +---------------+
| |
| shared memory buffers (SHM FD) |
| attached over UDS via |
| SCM_RIGHTS |
v v
+------------------+ +-----------------+
| soapy.IQ-info | | wfmd.audio-info |
+------------------+ +-----------------+
|
v
+------------------+
| audiosink.so |
| audio playback |
+------------------+
There are 3 important concepts:
A feed is a named channel in the broker. Add-ons publish to feeds or subscribe to them.
Examples:
soapy.config.in/outwfmd.audio-infosoapy.IQ-infodummy.foo(custom)
Messages are JSON frames over UDS. They may include file descriptors when announcing shared-memory buffers.
Add-ons are .so files implementing:
- feed registration
- command parsing
- publish/subscribe
- SHM ring creation and consumption
- DSP / decode loops
Add-ons are totally decoupled; wiring is explicit and chosen by the user.
-
Pure C11 /
pthread/dlopen -
UDS-based pub/sub broker
-
Dynamic addon discovery and hot loading
-
Zero-copy shared memory ring buffers (IQ + PCM)
-
File descriptors passed with
SCM_RIGHTS -
SoapySDR add-on for live RF capture
-
Wide FM demod add-on
-
Audio sink add-on (ALSA)
-
Dummy add-on for authors
-
Minimal CLI
-
New normalized ABI with usage-tagged routing:
subscribe <usage> <feed> unsubscribe <usage>Examples:
subscribe iq-source soapy.IQ-infosubscribe pcm-source wfmd.audio-info
Status: IQ → WFM → audio pipeline fully functional. ABI stabilizing. SHM rings normalized across addons.
This is the “real RF” demo.
cd PhaseHound/
make./ph-coreExample output:
[INF] core listening on /tmp/phasehound-broker.sock
[INF] loaded plugin soapy
[INF] loaded plugin wfmd
[INF] loaded plugin audiosink
./ph-cli list feeds
./ph-cli list addonsExample: FM broadcast at 100 MHz, 2.4 MS/s:
./ph-cli pub soapy.config.in \
"set sr=2400000 cf=100.0e6 bw=1.5e6"
./ph-cli pub soapy.config.in "select 0"
./ph-cli pub soapy.config.in "start"This allocates an IQ SHM ring and announces it on:
soapy.IQ-info
./ph-cli pub wfmd.config.in "subscribe iq-source soapy.IQ-info"
./ph-cli pub soapy.config.in "open" # republish memfd for newcomers./ph-cli pub wfmd.config.in "open"./ph-cli pub audiosink.config.in "subscribe pcm-source wfmd.audio-info"./ph-cli pub wfmd.config.in "start"
./ph-cli pub audiosink.config.in "start"You should now hear FM audio.
Load the dummy addon:
./ph-cli load addon dummyHelp:
./ph-cli pub dummy.config.in "help"Subscribe to its outputs:
./ph-cli sub dummy.config.out
./ph-cli sub dummy.fooDemo SHM feed:
./ph-cli pub dummy.config.in "shm-demo"Dummy illustrates:
- config feeds
- arbitrary feeds
- usage-tagged subscription
- SHM creation
- JSON replies
./ph-cli list addons
./ph-cli list feeds
./ph-cli load addon <name>
./ph-cli unload addon <name>
# publish a control command
./ph-cli pub <feed> "<text>"
# subscribe to feed(s)
./ph-cli sub <feedA> [<feedB> ...]You can subscribe to multiple feeds simultaneously, very useful for watching data flow live.
- JSON messages over UDS
- Typically on
<addon>.config.in/out - Human-readable debugging
- File descriptors sent over UDS using
SCM_RIGHTS - IQ and PCM streams are shared memory ring buffers
- Add-ons use atomic cursors for lock-free streaming
- Zero copies, stable performance
This avoids the classic “push samples through JSON/Python” problem.
Required
- Linux / POSIX
- gcc/clang
- pthread
- dlopen
Optional
- SoapySDR (for hardware RF)
Without SoapySDR, everything still builds — only hardware input is missing.
- ✅ Core broker
- ✅ Pub/sub model
- ✅ CLI
- ✅ SHM ring buffers
- ✅ SoapySDR source
- ✅ WFM demod
- ✅ Audio sink
- ✅ Dummy
- 🔄 ABI normalization (feeds + SHM + usage-tagged routing)
- 🔄 Documentation unification
- 🔲 Digital voice (DMR, TETRA, P25…)
- 🔲 File/network IQ sources
- 🔲 Remote access over TCP
- 🔲 CI + sanitizers + fuzzers
Long-term vision:
- PhaseHound becomes the “radio daemon” you leave running
- You control it via CLI or a lightweight UI
- DSP/decoders shipped as modular plugins
- Serious RF signal chains built without monolithic SDR apps
Contributions welcome:
- new demods
- new sinks
- protocol decoders
- SHM optimizations
- broker improvements
- bugfixes / docs updates
Code stays clean C11 — no magic, no bloated frameworks.
Open an issue or PR if:
- docs or ABI drift
- protocol needs clarity
- you wrote a cool addon