Demodulator Plugins
The source code of the demodulator plugins is available
here
.
In this example we demonstrate how to implement multiple plugins in the same .NET assembly, how to create the plugins that are used in DSP Pipeline, and how to make use of the DspFun library for signal processing.
Three Plugins in the Same Assembly
The three demodulators presented here demodulate CW, SSB and RTTY respectively.
The demodulation process is the same for all three modes, the difference
is only in the parameter values. Demodulation is implemented in the base class,
BaseDemodulator
, each plugin inherits from that class and changes the
default values of the
Pitch
and Sideband
parameters, as well as the plugin and mode names:
public abstract class BaseDemodulator : IPlugin, IDemodulator
{
...
}
[Export(typeof(IPlugin))]
[Export(typeof(IDemodulator))]
public class CwDemodulator : BaseDemodulator
{
public CwDemodulator()
{
pluginName = "CW Demodulator";
modeName = "CW";
settings.Pitch = 600;
settings.Sideband = Sideband.Upper;
}
}
[Export(typeof(IPlugin))]
[Export(typeof(IDemodulator))]
public class SsbDemodulator : BaseDemodulator
{
public SsbDemodulator()
{
pluginName = "SSB Demodulator";
modeName = "SSB";
settings.Pitch = 0;
settings.Sideband = Sideband.Upper;
}
}
[Export(typeof(IPlugin))]
[Export(typeof(IDemodulator))]
public class RttyDemodulator : BaseDemodulator
{
public RttyDemodulator()
{
pluginName = "RTTY Demodulator";
modeName = "RTTY";
settings.Pitch = 2125;
settings.Sideband = Sideband.Lower;
}
...
}
The derived classes export IPlugin
,
which turns them into plugins, and IDEmodulator
, so that
DSP Pipeline
can use them for demodulation.
Demodulation
The demodulation process includes these steps:
- resample the input I/Q signal down to 12 kHz, the standard sampling rate for audio processing;
- flip the sideband if necessary;
- if
Pitch
is non-zero, mix the signal up to the pitch frequency; - apply a 0..6 kHz filter to suppress the lower sideband.
The DspFun library that comes with Ham Cockpit has classes for all these signal processing tasks, which makes our job much easier. Moreover, these classes implement the ISampleStream interface, so they are easy to cascade.
IDemodulator : IInitSampleStream
IInitSampleStream.Initialize
The
Initialize
method implemented by the plugins
creates objects that will preform the signal processing tasks
and connects them in a chain:
private ISampleStream signal;
...
public void Initialize(ISampleStream source)
{
//input signal
signal = source;
//resampler
Resampler resampler = new Resampler(SignalFormat.AUDIO_SAMPLING_RATE, 30);
resampler.Initialize(signal);
signal = resampler;
//sideband flipper
flipper = new SidebandFlipper();
flipper.Initialize(signal);
signal = flipper;
//mixer
mixer = new Mixer(settings.Pitch);
mixer.Initialize(signal);
signal = mixer;
//filter
float Fc = 2962f / SignalFormat.AUDIO_SAMPLING_RATE;
var realTaps = Dsp.BlackmanSincKernel(Fc, 235);
Complex32[] taps = Dsp.FloatToComplex32(realTaps);
Dsp.Mix(taps, 0.25); //shift filter passband -3..3 kHz -> 0..6 kHz
filter = new ComplexFirFilter(taps);
filter.Initialize(signal);
signal = filter;
}
The ComplexFirFilter
needs an array of filter taps as an input
parameter to the constructor. We use the helper methods from
DspFun
to generate the taps.
IDemodulator : ISampleStream
ISampleStream.Format
The output from the demodulator is real-valued audio data filtered
to 0..6000 Hz. To create the Format property that describes the output format of
the demodulator, signal.Format
of the last processing stage
is used as a prototype, and the new parameters are set as follows:
format = new SignalFormat(signal.Format)
{
IsComplex = false,
PassbandLow = 0,
PassbandHigh = 6000,
Sideband = settings.Sideband
};
ISampleStream.Read
The Read
method of the demodulator is called by the next plugin in the DSP Pipeline
chain. When this method is called, the demodulator calls Read
on the previous plugin in the chain to get input data, and
uses its own signal processing blocks to produce
downsampled, freq-shifted, sideband-flipped and 6kHz-filtered I/Q
data. The final step is to discard the imaginary part of
the I/Q values and make it real-valued data:
public int Read(float[] buffer, int offset, int count)
{
//input I/Q
int read = count * Dsp.COMPONENTS_IN_COMPLEX;
if (inputBuffer == null || inputBuffer.Length < read) inputBuffer = new float[read];
//read downsampled, freq-shifted, sideband-flipped and 6kHz-filtered I/Q
read = signal.Read(inputBuffer, 0, read);
count = read / Dsp.COMPONENTS_IN_COMPLEX;
//complex to real
for (int i = 0; i < count; i++) buffer[offset + i] = inputBuffer[i * Dsp.COMPONENTS_IN_COMPLEX] * 100f;
SamplesAvailable?.Invoke(this, new SamplesAvailableEventArgs(buffer, offset, count));
return count;
}
IDemodulator : IModeSwitch
IModeSwitch.Mode
The Mode
property returns the name of the mode demodulated by
the plugin. The setter is not implemented since each demodulator
supports only one mode:
public string Mode { get => modeName; set => throw new NotImplementedException(); }
IModeSwitch.Sideband
The Sideband
property is read-write, it reflects the Enabled
state
of SidebandFlipper
used in the demodulator:
public Sideband Sideband { get => settings.Sideband; set => SetSideband(value); }