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
+33 -1
View File
@@ -233,6 +233,23 @@ void create_rxa (int channel)
max(2048, ch[channel].dsp_size), // number of coefficients for noise filter
0); // minimum phase flag
// WFM demod
rxa[channel].wfmd.p = create_wfmd (
0, // run
ch[channel].dsp_size, // buffer size
rxa[channel].midbuff, // pointer to input buffer
rxa[channel].midbuff, // pointer to output buffer
ch[channel].dsp_rate, // sample rate
75000.0, // deviation
20.0, // f_low
15000.0, // f_high
0.02, // tau - for dc removal
1, // run de-emphasis
75.0e-6, // de-emphasis time constant
0.5, // audio gain
max(2048, ch[channel].dsp_size), // # coefs for audio cutoff filter
0); // min phase flag for audio cutoff filter
// snba
rxa[channel].snba.p = create_snba (
0, // run
@@ -579,6 +596,7 @@ void destroy_rxa (int channel)
destroy_anf (rxa[channel].anf.p);
destroy_eqp (rxa[channel].eqp.p);
destroy_snba (rxa[channel].snba.p);
destroy_wfmd (rxa[channel].wfmd.p);
destroy_fmsq (rxa[channel].fmsq.p);
destroy_fmd (rxa[channel].fmd.p);
destroy_amd (rxa[channel].amd.p);
@@ -614,6 +632,7 @@ void flush_rxa (int channel)
flush_amd (rxa[channel].amd.p);
flush_fmd (rxa[channel].fmd.p);
flush_fmsq (rxa[channel].fmsq.p);
flush_wfmd (rxa[channel].wfmd.p);
flush_snba (rxa[channel].snba.p);
flush_eqp (rxa[channel].eqp.p);
flush_anf (rxa[channel].anf.p);
@@ -649,6 +668,7 @@ void xrxa (int channel)
xamd (rxa[channel].amd.p);
xfmd (rxa[channel].fmd.p);
xfmsq (rxa[channel].fmsq.p);
xwfmd (rxa[channel].wfmd.p);
xbpsnbain (rxa[channel].bpsnba.p, 1);
xbpsnbaout (rxa[channel].bpsnba.p, 1);
xsnba (rxa[channel].snba.p);
@@ -733,6 +753,7 @@ void setDSPSamplerate_rxa (int channel)
setSamplerate_fmd (rxa[channel].fmd.p, ch[channel].dsp_rate);
setBuffers_fmsq (rxa[channel].fmsq.p, rxa[channel].midbuff, rxa[channel].midbuff, rxa[channel].fmd.p->audio);
setSamplerate_fmsq (rxa[channel].fmsq.p, ch[channel].dsp_rate);
setSamplerate_wfmd (rxa[channel].wfmd.p, ch[channel].dsp_rate);
setSamplerate_snba (rxa[channel].snba.p, ch[channel].dsp_rate);
setSamplerate_eqp (rxa[channel].eqp.p, ch[channel].dsp_rate);
setSamplerate_anf (rxa[channel].anf.p, ch[channel].dsp_rate);
@@ -794,6 +815,8 @@ void setDSPBuffsize_rxa (int channel)
setSize_fmd (rxa[channel].fmd.p, ch[channel].dsp_size);
setBuffers_fmsq (rxa[channel].fmsq.p, rxa[channel].midbuff, rxa[channel].midbuff, rxa[channel].fmd.p->audio);
setSize_fmsq (rxa[channel].fmsq.p, ch[channel].dsp_size);
setBuffers_wfmd (rxa[channel].wfmd.p, rxa[channel].midbuff, rxa[channel].midbuff);
setSize_wfmd (rxa[channel].wfmd.p, ch[channel].dsp_size);
setBuffers_snba (rxa[channel].snba.p, rxa[channel].midbuff, rxa[channel].midbuff);
setSize_snba (rxa[channel].snba.p, ch[channel].dsp_size);
setBuffers_eqp (rxa[channel].eqp.p, rxa[channel].midbuff, rxa[channel].midbuff);
@@ -857,6 +880,7 @@ void SetRXAMode (int channel, int mode)
rxa[channel].mode = mode;
rxa[channel].amd.p->run = 0;
rxa[channel].fmd.p->run = 0;
rxa[channel].wfmd.p->run = 0;
rxa[channel].agc.p->run = 1;
switch (mode)
{
@@ -875,6 +899,10 @@ void SetRXAMode (int channel, int mode)
rxa[channel].fmd.p->run = 1;
rxa[channel].agc.p->run = 0;
break;
case RXA_WFM:
rxa[channel].wfmd.p->run = 1;
rxa[channel].agc.p->run = 0;
break;
default:
break;
@@ -961,13 +989,14 @@ void RXAbpsnbaCheck (int channel, int mode, int notch_run)
run_notches = 0;
break;
case RXA_FM:
case RXA_WFM:
f_low = +a->abs_low_freq;
f_high = +a->abs_high_freq;
run_notches = 0;
break;
case RXA_DRM:
case RXA_SPEC:
break;
}
// 'run' and 'position' are examined at run time; no filter changes required.
@@ -1010,6 +1039,7 @@ void RXAbpsnbaSet (int channel)
a->position = 1;
break;
case RXA_FM:
case RXA_WFM:
a->run = rxa[channel].snba.p->run;
a->position = 1;
break;
@@ -1046,6 +1076,7 @@ void RXASetNC (int channel, int nc)
SetRXAFMSQNC (channel, nc);
SetRXAFMNCde (channel, nc);
SetRXAFMNCaud (channel, nc);
SetRXAWFMNCaud (channel, nc);
SetChannelState (channel, oldstate, 0);
}
@@ -1059,4 +1090,5 @@ void RXASetMP (int channel, int mp)
SetRXAFMSQMP (channel, mp);
SetRXAFMMPde (channel, mp);
SetRXAFMMPaud (channel, mp);
SetRXAWFMMPaud (channel, mp);
}