Two decoding APIs are available:
| API | Use when |
|---|---|
ProtocolRegistry + IProtocol |
Protocol-first: you know the mode and want timing + decode in one object |
DecoderEngine |
You manage timing yourself; feed raw samples to a mode decoder |
using HamDigiSharp.Protocols;
using HamDigiSharp.Models;
IProtocol proto = ProtocolRegistry.Get(DigitalMode.FT8);
var decoder = proto.CreateDecoder();
// samples: float[], 15 s at 12 000 Hz, peak amplitude ±1
var results = decoder.Decode(samples, proto.DefaultFreqLow, proto.DefaultFreqHigh, "143000");
foreach (var r in results)
Console.WriteLine(r); // "143000 -07 0.3 1234 CQ W1AW FN42"DecoderEngine manages all mode decoders and forwards ResultAvailable events.
using HamDigiSharp.Engine;
using HamDigiSharp.Models;
using var engine = new DecoderEngine();
engine.Configure(new DecoderOptions
{
MyCall = "W1AW", // enables AP (a-priori) decoding passes
HisCall = "K1JT", // callsign of the station you are working
DecoderDepth = DecoderDepth.Normal,
ApDecode = true,
MaxCandidates = 500,
MinSyncDb = 2.5f,
QsoProgress = QsoProgress.None,
});
engine.ResultAvailable += r =>
Console.WriteLine($"{r.UtcTime} {r.Snr,4:+#;-#;0} dB {r.FrequencyHz,7:F1} Hz {r.Message}");
// Decode one 15-second FT8 period (12 000 Hz float PCM)
var results = engine.Decode(samples, DigitalMode.FT8, freqLow: 200, freqHigh: 3000, utcTime: "130000");| Property | Type | Default | Description |
|---|---|---|---|
MyCall |
string? |
null |
Your callsign — enables AP decode passes |
HisCall |
string? |
null |
Callsign you are working |
DecoderDepth |
DecoderDepth |
Normal |
LDPC OSD depth (see below) |
ApDecode |
bool |
false |
Run additional a-priori passes |
MaxCandidates |
int |
500 |
Sync candidates per period |
MinSyncDb |
float |
2.5 |
Minimum sync score to attempt decode |
QsoProgress |
QsoProgress |
None |
AP bias toward expected exchange tokens |
AveragingEnabled |
bool |
true |
Accumulate LLRs across consecutive periods (FT4, FT2, Q65) |
AveragingPeriods |
int |
3 |
Max periods to accumulate (Q65 only; FT4/FT2 accumulate until decode) |
| Value | OSD passes | Use case |
|---|---|---|
Fast |
None (BP only) | Real-time, low CPU |
Normal |
BP + OSD-1 | Balanced (default) |
Deep |
BP + OSD-2 | Maximum sensitivity |
Used to bias AP decoding toward the message type you expect next in the QSO flow:
None → Called → ReportReceived → RrrReceived → Completed
| Property | Type | Description |
|---|---|---|
UtcTime |
string |
Six-digit UTC stamp (HHMMSS) or period label |
Snr |
float |
Signal-to-noise ratio in dB |
DeltaTime |
float |
Time offset relative to nominal period start (s) |
FrequencyHz |
double |
Carrier/sync tone frequency in Hz |
Message |
string |
Decoded text |
See HamDigiSharp.Example/Program.cs for a complete working example that:
- Loads any WAV file (auto-resampled)
- Splits into mode-length periods
- Decodes all periods and prints results
Q65 improves sensitivity for very weak signals (EME, tropo) by coherently averaging up to 5 successive periods:
// Pass the same DecoderOptions instance for consecutive periods.
// The Q65 decoder accumulates LLRs internally and re-tries LDPC after each period.
var opts = new DecoderOptions { DecoderDepth = DecoderDepth.Deep };
var q65decoder = ProtocolRegistry.Get(DigitalMode.Q65A).CreateDecoder();
q65decoder.Configure(opts);
for (int i = 0; i < 5; i++)
{
var r = q65decoder.Decode(GetNextPeriod(), 200, 3000, $"period{i}");
if (r.Count > 0) break; // decoded — no need for more averages
}FT4 and FT2 support coherent LLR averaging across consecutive periods using the same
mechanism. When AveragingEnabled is true (the default) the decoder accumulates
normalised log-likelihood ratios at each candidate frequency. LDPC is retried after
every period, so a decode that fails on a single pass can succeed once enough periods
have been averaged.
// AveragingEnabled = true is the default — just create and configure once.
var decoder = ProtocolRegistry.Get(DigitalMode.FT4).CreateDecoder();
decoder.Configure(new DecoderOptions { DecoderDepth = DecoderDepth.Normal });
// Feed consecutive periods; the decoder accumulates internally.
foreach (var period in periods)
{
var results = decoder.Decode(period, 200, 3000, utcTime);
foreach (var r in results) Console.WriteLine(r);
// Accumulator is cleared automatically after each successful decode, so
// stale LLR from a disappeared signal cannot produce false decodes.
}Robustness guarantees:
- Accumulator cleared immediately after a successful decode.
- Frequencies not seen for more than 2 consecutive periods are expired and removed.
- Set
AveragingEnabled = false(orClearAverage = trueon one call) to reset state.
Expected sensitivity gain: each additional period contributes ~1.5 dB (√N law). Typical operating range: −17 dB single-period floor → −20 dB with 4 periods averaged.
WSPR uses a two-pass pipeline with signal subtraction to recover signals that would be masked by stronger co-channel signals:
Pass 1 (Fano-only)
├─ decode all candidates with Fano
├─ add callsigns to AP hash
└─ subtract decoded signals from the complex baseband
Pass 2 (Fano + OSD, AP-gated)
├─ re-detect candidates from cleaned baseband
├─ try Fano first
├─ if Fano fails → OSD fallback (depth=1)
└─ OSD result only accepted if callsign is in AP hash (prevents phantoms)
The AP hash persists across Decode() calls on the same decoder instance,
accumulating known callsigns over the receive session and improving sensitivity
for stations seen in earlier periods.
var decoder = new WsprDecoder();
// Period 1 — strong station W1AW decoded; hash now contains "W1AW"
var r1 = decoder.Decode(period1, 1400, 1600, "000000");
// Period 2 — W1AW is weaker; OSD accepts it because hash contains "W1AW"
var r2 = decoder.Decode(period2, 1400, 1600, "000200");
// Start fresh (e.g., new band or new session)
decoder.Reset();WsprConv.OsdDecode(softSymbols, depth: 1, out decoded):
depth |
Candidates | Pre-screen | Approx. time |
|---|---|---|---|
1 |
K+1 = 51 | ntheta=16 | <5 ms |
2 |
~1276 | ntheta=22 | ~50 ms |
The decoder uses depth=1 by default (real-time safe).