wfm: add a wideband FM modulator and demodulator
Model the pair on fmd.c / fmmod.c, but with the parts that a 75 kHz
deviation forces:
- the demodulator discriminates with arg(x[n] * conj(x[n-1])) rather
than a PLL; an omegaN in the tens of kHz cannot track 75 kHz.
- emphasis is a one-pole RC (tau 75 us) on both ends, not fmd's 1/f
fc_impulse FIR. A 1/f FIR from f_low = 20 Hz would sit ~+57 dB at
20 Hz, where broadcast FM specifies flat below the corner.
- TXA_WFM leaves the shared preemph block off; wfmmod carries its own.
- wfmmod clamps bp_fc = deviation + f_high to 0.45 * samplerate, since
+/-90 kHz exceeds Nyquist at the rates the narrowband modes use.
Scope is mono: no 19 kHz pilot, no 38 kHz stereo subcarrier, no RDS.
Both mode enums are appended to so the ABI stays stable for clients.
The JNI bindings are deliberately left alone.
Verified against a wfmmod -> wfmd loopback: the discriminator is exact,
the dc-removal one-pole tracks |H_lp(f)| * ain * sdelta to ratio 1.000,
and a 700 + 1900 Hz two-tone comes back with THD+N ~ 3e-5 % at +/-37.5
kHz deviation. Note the demodulator aliases unless samplerate exceeds
2 * deviation * |aud|max, so 192 kHz is the practical floor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
/* wfmd.c
|
||||
|
||||
This file is part of a program that implements a Software-Defined Radio.
|
||||
|
||||
Copyright (C) 2013, 2023 Warren Pratt, NR0V
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
The author can be reached by email at
|
||||
|
||||
warren@wpratt.com
|
||||
|
||||
*/
|
||||
|
||||
#include "comm.h"
|
||||
|
||||
void calc_wfmd (WFMD a)
|
||||
{
|
||||
// discriminator
|
||||
a->pre_i = 0.0;
|
||||
a->pre_q = 0.0;
|
||||
a->again = a->rate / (a->deviation * TWOPI);
|
||||
// dc removal
|
||||
a->mtau = exp(-1.0 / (a->rate * a->tau));
|
||||
a->onem_mtau = 1.0 - a->mtau;
|
||||
a->fmdc = 0.0;
|
||||
// de-emphasis
|
||||
if (a->tau_de > 0.0) a->mde = exp(-1.0 / (a->rate * a->tau_de));
|
||||
else a->mde = 0.0;
|
||||
a->onem_mde = 1.0 - a->mde;
|
||||
a->deemph_z = 0.0;
|
||||
// detector limiter
|
||||
a->plim = create_wcpagc (
|
||||
1, // run - always ON
|
||||
5, // mode
|
||||
1, // 0 for max(I,Q), 1 for envelope
|
||||
a->out, // input buff pointer
|
||||
a->out, // output buff pointer
|
||||
a->size, // io_buffsize
|
||||
(int)a->rate, // sample rate
|
||||
0.001, // tau_attack
|
||||
0.008, // tau_decay
|
||||
4, // n_tau
|
||||
a->lim_gain, // max_gain (sets threshold, initial value)
|
||||
1.0, // var_gain / slope
|
||||
1.0, // fixed_gain
|
||||
1.0, // max_input
|
||||
0.9, // out_targ
|
||||
0.250, // tau_fast_backaverage
|
||||
0.004, // tau_fast_decay
|
||||
4.0, // pop_ratio
|
||||
0, // hang_enable
|
||||
0.500, // tau_hang_backmult
|
||||
0.500, // hangtime
|
||||
2.000, // hang_thresh
|
||||
0.100); // tau_hang_decay
|
||||
}
|
||||
|
||||
void decalc_wfmd (WFMD a)
|
||||
{
|
||||
destroy_wcpagc(a->plim);
|
||||
}
|
||||
|
||||
WFMD create_wfmd (int run, int size, double* in, double* out, int rate, double deviation, double f_low, double f_high,
|
||||
double tau, int deemph_run, double tau_de, double afgain, int nc_aud, int mp_aud)
|
||||
{
|
||||
WFMD a = (WFMD) malloc0 (sizeof (wfmd));
|
||||
double* impulse;
|
||||
a->run = run;
|
||||
a->size = size;
|
||||
a->in = in;
|
||||
a->out = out;
|
||||
a->rate = (double)rate;
|
||||
a->deviation = deviation;
|
||||
a->f_low = f_low;
|
||||
a->f_high = f_high;
|
||||
a->tau = tau;
|
||||
a->deemph_run = deemph_run;
|
||||
a->tau_de = tau_de;
|
||||
a->afgain = afgain;
|
||||
a->nc_aud = nc_aud;
|
||||
a->mp_aud = mp_aud;
|
||||
a->lim_run = 0;
|
||||
a->lim_pre_gain = 0.4;
|
||||
a->lim_gain = 2.5;
|
||||
calc_wfmd (a);
|
||||
a->audio = (double *) malloc0 (a->size * sizeof (complex));
|
||||
// audio filter
|
||||
impulse = fir_bandpass(a->nc_aud, 0.8 * a->f_low, 1.1 * a->f_high, a->rate, 0, 1, a->afgain / (2.0 * a->size));
|
||||
a->paud = create_fircore (a->size, a->audio, a->out, a->nc_aud, a->mp_aud, impulse);
|
||||
_aligned_free (impulse);
|
||||
return a;
|
||||
}
|
||||
|
||||
void destroy_wfmd (WFMD a)
|
||||
{
|
||||
destroy_fircore (a->paud);
|
||||
_aligned_free (a->audio);
|
||||
decalc_wfmd (a);
|
||||
_aligned_free (a);
|
||||
}
|
||||
|
||||
void flush_wfmd (WFMD a)
|
||||
{
|
||||
memset (a->audio, 0, a->size * sizeof (complex));
|
||||
flush_fircore (a->paud);
|
||||
a->pre_i = 0.0;
|
||||
a->pre_q = 0.0;
|
||||
a->fmdc = 0.0;
|
||||
a->deemph_z = 0.0;
|
||||
flush_wcpagc (a->plim);
|
||||
}
|
||||
|
||||
void xwfmd (WFMD a)
|
||||
{
|
||||
if (a->run)
|
||||
{
|
||||
int i;
|
||||
double si, sq, cr, ci, det, aud;
|
||||
for (i = 0; i < a->size; i++)
|
||||
{
|
||||
// quadrature discriminator: det = arg (x[n] * conj (x[n-1]))
|
||||
si = a->in[2 * i + 0];
|
||||
sq = a->in[2 * i + 1];
|
||||
cr = + si * a->pre_i + sq * a->pre_q;
|
||||
ci = - si * a->pre_q + sq * a->pre_i;
|
||||
a->pre_i = si;
|
||||
a->pre_q = sq;
|
||||
det = atan2 (ci, cr);
|
||||
// dc removal, gain, & demod output
|
||||
a->fmdc = a->mtau * a->fmdc + a->onem_mtau * det;
|
||||
aud = a->again * (det - a->fmdc);
|
||||
// de-emphasis
|
||||
if (a->deemph_run)
|
||||
{
|
||||
a->deemph_z = a->mde * a->deemph_z + a->onem_mde * aud;
|
||||
aud = a->deemph_z;
|
||||
}
|
||||
a->audio[2 * i + 0] = aud;
|
||||
a->audio[2 * i + 1] = aud;
|
||||
}
|
||||
// audio filter
|
||||
xfircore (a->paud);
|
||||
if (a->lim_run)
|
||||
{
|
||||
for (i = 0; i < 2 * a->size; i++)
|
||||
a->out[i] *= a->lim_pre_gain;
|
||||
xwcpagc (a->plim);
|
||||
}
|
||||
}
|
||||
else if (a->in != a->out)
|
||||
memcpy (a->out, a->in, a->size * sizeof (complex));
|
||||
}
|
||||
|
||||
void setBuffers_wfmd (WFMD a, double* in, double* out)
|
||||
{
|
||||
decalc_wfmd (a);
|
||||
a->in = in;
|
||||
a->out = out;
|
||||
calc_wfmd (a);
|
||||
setBuffers_fircore (a->paud, a->audio, a->out);
|
||||
setBuffers_wcpagc (a->plim, a->out, a->out);
|
||||
}
|
||||
|
||||
void setSamplerate_wfmd (WFMD a, int rate)
|
||||
{
|
||||
double* impulse;
|
||||
decalc_wfmd (a);
|
||||
a->rate = rate;
|
||||
calc_wfmd (a);
|
||||
// audio filter
|
||||
impulse = fir_bandpass(a->nc_aud, 0.8 * a->f_low, 1.1 * a->f_high, a->rate, 0, 1, a->afgain / (2.0 * a->size));
|
||||
setImpulse_fircore (a->paud, impulse, 1);
|
||||
_aligned_free (impulse);
|
||||
setSamplerate_wcpagc (a->plim, (int)a->rate);
|
||||
}
|
||||
|
||||
void setSize_wfmd (WFMD a, int size)
|
||||
{
|
||||
double* impulse;
|
||||
decalc_wfmd (a);
|
||||
_aligned_free (a->audio);
|
||||
a->size = size;
|
||||
calc_wfmd (a);
|
||||
a->audio = (double *) malloc0 (a->size * sizeof (complex));
|
||||
// audio filter
|
||||
destroy_fircore (a->paud);
|
||||
impulse = fir_bandpass(a->nc_aud, 0.8 * a->f_low, 1.1 * a->f_high, a->rate, 0, 1, a->afgain / (2.0 * a->size));
|
||||
a->paud = create_fircore (a->size, a->audio, a->out, a->nc_aud, a->mp_aud, impulse);
|
||||
_aligned_free (impulse);
|
||||
setSize_wcpagc (a->plim, a->size);
|
||||
}
|
||||
|
||||
/********************************************************************************************************
|
||||
* *
|
||||
* RXA Properties *
|
||||
* *
|
||||
********************************************************************************************************/
|
||||
|
||||
PORT
|
||||
void SetRXAWFMDeviation (int channel, double deviation)
|
||||
{
|
||||
WFMD a;
|
||||
EnterCriticalSection (&ch[channel].csDSP);
|
||||
a = rxa[channel].wfmd.p;
|
||||
a->deviation = deviation;
|
||||
a->again = a->rate / (a->deviation * TWOPI);
|
||||
LeaveCriticalSection (&ch[channel].csDSP);
|
||||
}
|
||||
|
||||
PORT
|
||||
void SetRXAWFMNCaud (int channel, int nc)
|
||||
{
|
||||
WFMD a;
|
||||
double* impulse;
|
||||
EnterCriticalSection (&ch[channel].csDSP);
|
||||
a = rxa[channel].wfmd.p;
|
||||
if (a->nc_aud != nc)
|
||||
{
|
||||
a->nc_aud = nc;
|
||||
impulse = fir_bandpass(a->nc_aud, 0.8 * a->f_low, 1.1 * a->f_high, a->rate, 0, 1, a->afgain / (2.0 * a->size));
|
||||
setNc_fircore (a->paud, a->nc_aud, impulse);
|
||||
_aligned_free (impulse);
|
||||
}
|
||||
LeaveCriticalSection (&ch[channel].csDSP);
|
||||
}
|
||||
|
||||
PORT
|
||||
void SetRXAWFMMPaud (int channel, int mp)
|
||||
{
|
||||
WFMD a;
|
||||
a = rxa[channel].wfmd.p;
|
||||
if (a->mp_aud != mp)
|
||||
{
|
||||
a->mp_aud = mp;
|
||||
setMp_fircore (a->paud, a->mp_aud);
|
||||
}
|
||||
}
|
||||
|
||||
PORT
|
||||
void SetRXAWFMAFFilter (int channel, double low, double high)
|
||||
{
|
||||
WFMD a = rxa[channel].wfmd.p;
|
||||
double* impulse;
|
||||
EnterCriticalSection (&ch[channel].csDSP);
|
||||
if (a->f_low != low || a->f_high != high)
|
||||
{
|
||||
a->f_low = low;
|
||||
a->f_high = high;
|
||||
impulse = fir_bandpass (a->nc_aud, 0.8 * a->f_low, 1.1 * a->f_high, a->rate, 0, 1, a->afgain / (2.0 * a->size));
|
||||
setImpulse_fircore (a->paud, impulse, 1);
|
||||
_aligned_free (impulse);
|
||||
}
|
||||
LeaveCriticalSection (&ch[channel].csDSP);
|
||||
}
|
||||
|
||||
PORT
|
||||
void SetRXAWFMDeemphRun (int channel, int run)
|
||||
{
|
||||
WFMD a = rxa[channel].wfmd.p;
|
||||
EnterCriticalSection (&ch[channel].csDSP);
|
||||
if (a->deemph_run != run)
|
||||
{
|
||||
a->deemph_run = run;
|
||||
a->deemph_z = 0.0;
|
||||
}
|
||||
LeaveCriticalSection (&ch[channel].csDSP);
|
||||
}
|
||||
|
||||
PORT
|
||||
void SetRXAWFMDeemphTau (int channel, double tau)
|
||||
{
|
||||
WFMD a = rxa[channel].wfmd.p;
|
||||
EnterCriticalSection (&ch[channel].csDSP);
|
||||
if (a->tau_de != tau && tau > 0.0)
|
||||
{
|
||||
a->tau_de = tau;
|
||||
a->mde = exp(-1.0 / (a->rate * a->tau_de));
|
||||
a->onem_mde = 1.0 - a->mde;
|
||||
a->deemph_z = 0.0;
|
||||
}
|
||||
LeaveCriticalSection (&ch[channel].csDSP);
|
||||
}
|
||||
|
||||
PORT
|
||||
void SetRXAWFMLimRun (int channel, int run)
|
||||
{
|
||||
WFMD a = rxa[channel].wfmd.p;
|
||||
EnterCriticalSection (&ch[channel].csDSP);
|
||||
if (a->lim_run != run)
|
||||
{
|
||||
a->lim_run = run;
|
||||
}
|
||||
LeaveCriticalSection (&ch[channel].csDSP);
|
||||
}
|
||||
|
||||
PORT
|
||||
void SetRXAWFMLimGain (int channel, double gaindB)
|
||||
{
|
||||
double gain = pow(10.0, gaindB / 20.0);
|
||||
WFMD a = rxa[channel].wfmd.p;
|
||||
EnterCriticalSection (&ch[channel].csDSP);
|
||||
if (a->lim_gain != gain)
|
||||
{
|
||||
decalc_wfmd (a);
|
||||
a->lim_gain = gain;
|
||||
calc_wfmd (a);
|
||||
}
|
||||
LeaveCriticalSection (&ch[channel].csDSP);
|
||||
}
|
||||
Reference in New Issue
Block a user