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:
Uladzimir Karpenka
2026-07-10 07:43:07 +03:00
parent f39eea6b01
commit 65cb3c386e
12 changed files with 871 additions and 8 deletions
+250
View File
@@ -0,0 +1,250 @@
/* wfmmod.c
This file is part of a program that implements a Software-Defined Radio.
Copyright (C) 2013, 2016, 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"
// the modulated spectrum spans +/-(deviation + f_high); at the sample rates used for
// narrowband modes that exceeds Nyquist, so the bandpass degenerates to a passthrough.
static double bpfc_wfmmod (double samplerate, double deviation, double f_high)
{
double fc = deviation + f_high;
double max_fc = 0.45 * samplerate;
if (fc > max_fc) fc = max_fc;
return fc;
}
void calc_wfmmod (WFMMOD a)
{
// pre-emphasis
if (a->tau_pre > 0.0) a->pmult = exp(-1.0 / (a->samplerate * a->tau_pre));
else a->pmult = 0.0;
a->pnorm = 1.0 / (1.0 - a->pmult);
a->pre_z = 0.0;
// mod
a->sphase = 0.0;
a->sdelta = TWOPI * a->deviation / a->samplerate;
// bandpass
a->bp_fc = bpfc_wfmmod (a->samplerate, a->deviation, a->f_high);
}
WFMMOD create_wfmmod (int run, int size, double* in, double* out, int rate, double dev, double f_low, double f_high,
int pre_run, double tau_pre, int bp_run, int nc, int mp)
{
WFMMOD a = (WFMMOD) malloc0 (sizeof (wfmmod));
double* impulse;
a->run = run;
a->size = size;
a->in = in;
a->out = out;
a->samplerate = (double)rate;
a->deviation = dev;
a->f_low = f_low;
a->f_high = f_high;
a->pre_run = pre_run;
a->tau_pre = tau_pre;
a->bp_run = bp_run;
a->nc = nc;
a->mp = mp;
calc_wfmmod (a);
impulse = fir_bandpass(a->nc, -a->bp_fc, +a->bp_fc, a->samplerate, 0, 1, 1.0 / (2 * a->size));
a->p = create_fircore (a->size, a->out, a->out, a->nc, a->mp, impulse);
_aligned_free (impulse);
return a;
}
void destroy_wfmmod (WFMMOD a)
{
destroy_fircore (a->p);
_aligned_free (a);
}
void flush_wfmmod (WFMMOD a)
{
a->pre_z = 0.0;
a->sphase = 0.0;
flush_fircore (a->p);
}
void xwfmmod (WFMMOD a)
{
int i;
double aud, dp;
if (a->run)
{
for (i = 0; i < a->size; i++)
{
aud = a->in[2 * i + 0];
if (a->pre_run)
{
dp = a->pnorm * (aud - a->pmult * a->pre_z);
a->pre_z = aud;
aud = dp;
}
dp = aud * a->sdelta;
a->sphase += dp;
// at the wide deviation, |dp| exceeds TWOPI once samplerate < deviation,
// so one subtraction is not enough to bring sphase back into range
while (a->sphase >= TWOPI) a->sphase -= TWOPI;
while (a->sphase < 0.0 ) a->sphase += TWOPI;
a->out[2 * i + 0] = 0.7071 * cos (a->sphase);
a->out[2 * i + 1] = 0.7071 * sin (a->sphase);
}
if (a->bp_run)
xfircore (a->p);
}
else if (a->in != a->out)
memcpy (a->out, a->in, a->size * sizeof (complex));
}
void setBuffers_wfmmod (WFMMOD a, double* in, double* out)
{
a->in = in;
a->out = out;
calc_wfmmod (a);
setBuffers_fircore (a->p, a->out, a->out);
}
void setSamplerate_wfmmod (WFMMOD a, int rate)
{
double* impulse;
a->samplerate = rate;
calc_wfmmod (a);
impulse = fir_bandpass(a->nc, -a->bp_fc, +a->bp_fc, a->samplerate, 0, 1, 1.0 / (2 * a->size));
setImpulse_fircore (a->p, impulse, 1);
_aligned_free (impulse);
}
void setSize_wfmmod (WFMMOD a, int size)
{
double* impulse;
a->size = size;
calc_wfmmod (a);
setSize_fircore (a->p, a->size);
impulse = fir_bandpass(a->nc, -a->bp_fc, +a->bp_fc, a->samplerate, 0, 1, 1.0 / (2 * a->size));
setImpulse_fircore (a->p, impulse, 1);
_aligned_free (impulse);
}
/********************************************************************************************************
* *
* TXA Properties *
* *
********************************************************************************************************/
PORT
void SetTXAWFMDeviation (int channel, double deviation)
{
WFMMOD a = txa[channel].wfmmod.p;
double bp_fc = bpfc_wfmmod (a->samplerate, deviation, a->f_high);
double* impulse = fir_bandpass (a->nc, -bp_fc, +bp_fc, a->samplerate, 0, 1, 1.0 / (2 * a->size));
setImpulse_fircore (a->p, impulse, 0);
_aligned_free (impulse);
EnterCriticalSection (&ch[channel].csDSP);
a->deviation = deviation;
// mod
a->sphase = 0.0;
a->sdelta = TWOPI * a->deviation / a->samplerate;
// bandpass
a->bp_fc = bp_fc;
setUpdate_fircore (a->p);
LeaveCriticalSection (&ch[channel].csDSP);
}
PORT
void SetTXAWFMNC (int channel, int nc)
{
WFMMOD a;
double* impulse;
EnterCriticalSection (&ch[channel].csDSP);
a = txa[channel].wfmmod.p;
if (a->nc != nc)
{
a->nc = nc;
impulse = fir_bandpass (a->nc, -a->bp_fc, +a->bp_fc, a->samplerate, 0, 1, 1.0 / (2 * a->size));
setNc_fircore (a->p, a->nc, impulse);
_aligned_free (impulse);
}
LeaveCriticalSection (&ch[channel].csDSP);
}
PORT
void SetTXAWFMMP (int channel, int mp)
{
WFMMOD a;
a = txa[channel].wfmmod.p;
if (a->mp != mp)
{
a->mp = mp;
setMp_fircore (a->p, a->mp);
}
}
PORT
void SetTXAWFMAFFreqs (int channel, double low, double high)
{
WFMMOD a;
double* impulse;
EnterCriticalSection (&ch[channel].csDSP);
a = txa[channel].wfmmod.p;
if (a->f_low != low || a->f_high != high)
{
a->f_low = low;
a->f_high = high;
a->bp_fc = bpfc_wfmmod (a->samplerate, a->deviation, a->f_high);
impulse = fir_bandpass (a->nc, -a->bp_fc, +a->bp_fc, a->samplerate, 0, 1, 1.0 / (2 * a->size));
setImpulse_fircore (a->p, impulse, 1);
_aligned_free (impulse);
}
LeaveCriticalSection (&ch[channel].csDSP);
}
PORT
void SetTXAWFMPreEmphRun (int channel, int run)
{
WFMMOD a = txa[channel].wfmmod.p;
EnterCriticalSection (&ch[channel].csDSP);
if (a->pre_run != run)
{
a->pre_run = run;
a->pre_z = 0.0;
}
LeaveCriticalSection (&ch[channel].csDSP);
}
PORT
void SetTXAWFMPreEmphTau (int channel, double tau)
{
WFMMOD a = txa[channel].wfmmod.p;
EnterCriticalSection (&ch[channel].csDSP);
if (a->tau_pre != tau && tau > 0.0)
{
a->tau_pre = tau;
a->pmult = exp(-1.0 / (a->samplerate * a->tau_pre));
a->pnorm = 1.0 / (1.0 - a->pmult);
a->pre_z = 0.0;
}
LeaveCriticalSection (&ch[channel].csDSP);
}