V131R-260725:修正FSSI/FWD显示并优化8VSB频谱分析

修正FSSI/FWD标定、存储和LCD显示;增强8VSB场同步、MER分析及网页频谱展示;更新版本号与修改记录。
This commit is contained in:
2026-07-28 14:05:40 +08:00
parent 296c330197
commit 92b155499d
14 changed files with 1084 additions and 269 deletions
+686 -24
View File
@@ -231,15 +231,20 @@ static void build_bank(void)
}
/* ------------------------------------------------- resample + derotate */
/* c_k = y_k * j^k for k in [k0, k0+nsym), with tau0 = q/1539.
* Returns 0 if the window is too short. */
static int resample_syms(int k0, int nsym, int q)
/* c_k = y_k * j^k for k in [k0, k0+nsym), sample position of symbol k being
* s_ofs + (k*1430 + q)/1539, reading from sxi/sxq (xi/xq or the PR12
* Farrow-compensated copy). k0 may be negative when the grid is anchored at
* a field-sync peak (s_ofs = d0); (k0+k)&3 stays a correct mod-4 on two's
* complement. Returns 0 if a kernel would run off the window. */
static int resample_syms(const float *sxi, const float *sxq, int s_ofs,
int k0, int nsym, int q)
{
int k, num, n;
/* exact integer recurrence: sample position of symbol k is (k*1430 + q)/1539 */
/* exact integer recurrence, floor division for possibly-negative t */
long long t = (long long)k0 * SYM_NUM + q;
n = (int)(t / SYM_DEN);
num = (int)(t % SYM_DEN);
long long nf = (t >= 0) ? t / SYM_DEN : -((-t + SYM_DEN - 1) / SYM_DEN);
n = (int)nf + s_ofs;
num = (int)(t - nf * SYM_DEN);
for (k = 0; k < nsym; k++) {
const float *h = bank[num];
@@ -251,7 +256,7 @@ static int resample_syms(int k0, int nsym, int q)
/* Four independent accumulators. A single chain of `yi += h[m]*x[m]` is
* latency-bound: every VFP fmla waits on the previous one (~8 cycles), which
* is what made the demod take 1.8 s per window on the A7. PSI_TAPS is 36 = 4*9. */
{ const float *pi = xi + base, *pq = xq + base;
{ const float *pi = sxi + base, *pq = sxq + base;
float i0 = 0, i1 = 0, i2 = 0, i3 = 0, q0 = 0, q1 = 0, q2 = 0, q3 = 0;
for (m = 0; m < PSI_TAPS; m += 4) {
i0 += h[m + 0] * pi[m + 0]; q0 += h[m + 0] * pq[m + 0];
@@ -388,7 +393,7 @@ static float probe(int q, int k0, int nsym)
{
q %= SYM_DEN;
if (q < 0) q += SYM_DEN;
if (!resample_syms(k0, nsym, q)) return -99.0f;
if (!resample_syms(xi, xq, 0, k0, nsym, q)) return -99.0f;
apply_theta(nsym, theta_of(nsym));
return slice_snr(ci, nsym, NULL, NULL);
}
@@ -573,6 +578,619 @@ static int ms(struct timespec a, struct timespec b)
return (int)((b.tv_sec - a.tv_sec) * 1000 + (b.tv_nsec - a.tv_nsec) / 1000000);
}
/* ================================================================ PR12 =====
* Field-sync-driven timing: FFT correlation against segsync+PN511 -> peak d0
* -> early-late gate (dL/dR at +-1 sample) -> mu -> 16-tap Farrow fractional
* delay -> the unchanged demod anchored at d0 with q = 0.
* PC reference: Notes/vsb_pr12_reference.py; tables: inc/pr12_tables.h.
* Runs IN PARALLEL with find_tau for cross-validation (Task12.md section 3.6);
* snr_db_pub stays on the old chain until the switch. */
#include "pr12_tables.h"
#define FFTN 8192
#define CORR_STEP (FFTN - PR12_REF_LEN + 1) /* valid lags per block */
#define CORR_MARGIN (PR12_REF_LEN + 40)
static float fs_absr2[NSAMP]; /* |R|^2 per lag, 1 MB */
static float fft_re[FFTN], fft_im[FFTN];
static float refF_re[FFTN], refF_im[FFTN];
static float tw_re[FFTN / 2], tw_im[FFTN / 2];
static float xfi[NSAMP], xfq[NSAMP]; /* Farrow-compensated copy */
static int pr12_ready;
static void fft_c(float *re, float *im, int inv)
{
int i, j, len;
for (i = 1, j = 0; i < FFTN; i++) { /* bit reversal */
int bit = FFTN >> 1;
for (; j & bit; bit >>= 1) j ^= bit;
j |= bit;
if (i < j) {
float t;
t = re[i]; re[i] = re[j]; re[j] = t;
t = im[i]; im[i] = im[j]; im[j] = t;
}
}
for (len = 2; len <= FFTN; len <<= 1) {
int half = len >> 1, step = FFTN / len;
for (i = 0; i < FFTN; i += len) {
for (j = 0; j < half; j++) {
int wi = j * step;
float wr = tw_re[wi], wm = inv ? -tw_im[wi] : tw_im[wi];
float ur = re[i + j], ui = im[i + j];
float vr = re[i + j + half] * wr - im[i + j + half] * wm;
float vi = re[i + j + half] * wm + im[i + j + half] * wr;
re[i + j] = ur + vr; im[i + j] = ui + vi;
re[i + j + half] = ur - vr; im[i + j + half] = ui - vi;
}
}
}
if (inv) {
float s = 1.0f / FFTN;
for (i = 0; i < FFTN; i++) { re[i] *= s; im[i] *= s; }
}
}
static void pr12_init(void)
{
int i;
for (i = 0; i < FFTN / 2; i++) {
double a = -2.0 * M_PI * i / FFTN;
tw_re[i] = (float)cos(a); tw_im[i] = (float)sin(a);
}
for (i = 0; i < FFTN; i++) {
refF_re[i] = (i < PR12_REF_LEN) ? pr12_ref_i[i] : 0.0f;
refF_im[i] = (i < PR12_REF_LEN) ? pr12_ref_q[i] : 0.0f;
}
fft_c(refF_re, refF_im, 0);
pr12_ready = 1;
}
/* overlap-save cross-correlation; returns lag m0 of ref[0] (peak), or -1 */
static int fsync_corr_c(float *ratio_out)
{
int b, i, m0 = -1, nvalid = NSAMP - CORR_MARGIN;
double sum2 = 0.0;
float peak2 = 0.0f;
for (b = 0; b * CORR_STEP < NSAMP; b++) {
int base = b * CORR_STEP;
for (i = 0; i < FFTN; i++) {
int s = base + i;
fft_re[i] = (s < NSAMP) ? xi[s] : 0.0f;
fft_im[i] = (s < NSAMP) ? xq[s] : 0.0f;
}
fft_c(fft_re, fft_im, 0);
for (i = 0; i < FFTN; i++) { /* X * conj(REF) */
float ar = fft_re[i], ai = fft_im[i];
float br = refF_re[i], bi = refF_im[i];
fft_re[i] = ar * br + ai * bi;
fft_im[i] = ai * br - ar * bi;
}
fft_c(fft_re, fft_im, 1);
for (i = 0; i < CORR_STEP && base + i < NSAMP; i++)
fs_absr2[base + i] = fft_re[i] * fft_re[i] + fft_im[i] * fft_im[i];
}
for (i = 40; i < nvalid; i++) {
sum2 += fs_absr2[i];
if (fs_absr2[i] > peak2) { peak2 = fs_absr2[i]; m0 = i; }
}
*ratio_out = (sum2 > 0.0 && m0 >= 0)
? sqrtf(peak2 / (float)(sum2 / (nvalid - 40))) : 0.0f;
return m0;
}
/* second complete peak at m0 +- 241972 (present in ~8 % of windows); info only */
static int pr12_second_peak(int m0, int *spacing)
{
int c, best = -1;
float b2 = 0.25f * fs_absr2[m0];
static const int sgn[2] = {-1, 1};
for (c = 0; c < 2; c++) {
int lo = m0 + sgn[c] * PR12_FLD_SAMP - 5, i;
if (lo < 40 || lo + 10 >= NSAMP - CORR_MARGIN) continue;
for (i = lo; i <= lo + 10; i++)
if (fs_absr2[i] > b2) { b2 = fs_absr2[i]; best = i; }
}
if (best >= 0 && spacing) *spacing = abs(best - m0);
return best;
}
/* |R| at fractional symbol-0 position d: Farrow-shift the received segment,
* then a direct dot product against the reference (16.6k MAC, negligible) */
static float frac_corr_c(double d)
{
int di = (int)floor(d), m0, i, j;
float mu = (float)(d - di), hr[PR12_FAR_NTAP];
double ar = 0.0, ai = 0.0;
if (mu > 0.5f) { di++; mu -= 1.0f; }
m0 = di - PR12_REF_PAD;
if (m0 + PR12_FAR_M0 < 0 ||
m0 + PR12_REF_LEN + PR12_FAR_M0 + PR12_FAR_NTAP > NSAMP) return 0.0f;
for (j = 0; j < PR12_FAR_NTAP; j++) { /* Horner over mu */
float h = pr12_far_c[PR12_FAR_ORD][j];
for (i = PR12_FAR_ORD - 1; i >= 0; i--) h = h * mu + pr12_far_c[i][j];
hr[j] = h;
}
for (i = 0; i < PR12_REF_LEN; i++) {
const float *pi = xi + m0 + i + PR12_FAR_M0;
const float *pq = xq + m0 + i + PR12_FAR_M0;
float sr = 0.0f, sq = 0.0f;
for (j = 0; j < PR12_FAR_NTAP; j++) { sr += hr[j] * pi[j]; sq += hr[j] * pq[j]; }
ar += (double)pr12_ref_i[i] * sr + (double)pr12_ref_q[i] * sq;
ai += (double)pr12_ref_i[i] * sq - (double)pr12_ref_q[i] * sr;
}
return (float)sqrt(ar * ar + ai * ai);
}
/* parabolic init on |R(d0-1)|,|R(d0)|,|R(d0+1)|, then dL-dR iterations.
* Returns mu in (-0.5, 0.5]; *di_out is the re-centred integer position. */
static float early_late_c(int d0, int *di_out, int *it_out)
{
int m0 = d0 - PR12_REF_PAD, it, r;
float Rm = sqrtf(fs_absr2[m0 - 1]), R0 = sqrtf(fs_absr2[m0]);
float Rp = sqrtf(fs_absr2[m0 + 1]);
float den = Rm - 2.0f * R0 + Rp, mu, e = 1.0f;
mu = (fabsf(den) > 1e-20f) ? 0.5f * (Rm - Rp) / den : 0.0f;
if (mu > 0.75f) mu = 0.75f;
if (mu < -0.75f) mu = -0.75f;
for (it = 0; it < 8; it++) {
float dL = frac_corr_c((double)d0 + mu - 1.0);
float dR = frac_corr_c((double)d0 + mu + 1.0);
e = (dL - dR) / (dL + dR + 1e-20f);
if (fabsf(e) < PR12_EL_EPS) { it++; break; }
mu -= PR12_EL_GAMMA * e;
}
r = (int)lroundf(mu);
*di_out = d0 + r;
*it_out = it;
return mu - (float)r;
}
/* decision SNR at pos = di + (k*1430 + q)/1539 (q folded so 0 <= q < 1539) */
static float probe_fs(int di, int q, int k0, int nsym)
{
while (q < 0) { q += SYM_DEN; di--; }
while (q >= SYM_DEN) { q -= SYM_DEN; di++; }
if (!resample_syms(xi, xq, di, k0, nsym, q)) return -99.0f;
apply_theta(nsym, theta_of(nsym));
return slice_snr(ci, nsym, NULL, NULL);
}
/* Final polish: +-8/1539 micro-search on decision SNR (16384-symbol probes)
* around the early-late mu, parabolic refine. Anchored to THIS window's own
* field sync -- never warm-started across windows (find_tau's hard lesson). */
static float snr_trim_c(int di, float mu_el, int k_lo)
{
int q_el = (int)lroundf(mu_el * (float)SYM_DEN);
int j, jb = 0, k0 = k_lo + 2000;
float best = -1e30f, s[17], mu;
for (j = -8; j <= 8; j++) {
s[j + 8] = probe_fs(di, q_el + j, k0, 16384);
if (s[j + 8] > best) { best = s[j + 8]; jb = j; }
}
mu = (float)(q_el + jb) / (float)SYM_DEN;
if (jb > -8 && jb < 8) {
float y0 = s[jb + 7], y1 = s[jb + 8], y2 = s[jb + 9];
float den = y0 - 2.0f * y1 + y2, dd;
dd = (fabsf(den) > 1e-12f) ? 0.5f * (y0 - y2) / den : 0.0f;
if (dd > 1.0f) dd = 1.0f;
if (dd < -1.0f) dd = -1.0f;
mu += dd / (float)SYM_DEN;
}
return mu;
}
/* x(n+mu) over the whole window into xfi/xfq (constant mu -> plain 16-tap FIR;
* 4 accumulators for the same VFP-latency reason as resample_syms) */
static void farrow_apply(float mu)
{
int i, j;
float h[PR12_FAR_NTAP];
for (j = 0; j < PR12_FAR_NTAP; j++) {
float v = pr12_far_c[PR12_FAR_ORD][j];
for (i = PR12_FAR_ORD - 1; i >= 0; i--) v = v * mu + pr12_far_c[i][j];
h[j] = v;
}
for (i = 0; i < 8; i++) { xfi[i] = xi[i]; xfq[i] = xq[i]; }
for (i = NSAMP - 8; i < NSAMP; i++) { xfi[i] = xi[i]; xfq[i] = xq[i]; }
for (i = 8; i < NSAMP - 8; i++) {
const float *pi = xi + i + PR12_FAR_M0, *pq = xq + i + PR12_FAR_M0;
float i0 = 0, i1 = 0, i2 = 0, i3 = 0, q0 = 0, q1 = 0, q2 = 0, q3 = 0;
for (j = 0; j < PR12_FAR_NTAP; j += 4) {
i0 += h[j + 0] * pi[j + 0]; q0 += h[j + 0] * pq[j + 0];
i1 += h[j + 1] * pi[j + 1]; q1 += h[j + 1] * pq[j + 1];
i2 += h[j + 2] * pi[j + 2]; q2 += h[j + 2] * pq[j + 2];
i3 += h[j + 3] * pi[j + 3]; q3 += h[j + 3] * pq[j + 3];
}
xfi[i] = (i0 + i1) + (i2 + i3);
xfq[i] = (q0 + q1) + (q2 + q3);
}
}
/* Error power spectrum: the frequency distribution of the raw decision error
* e_k = r_k - slice(r_k), Welch-averaged (Hann, 8192-symbol blocks reusing the
* field-sync FFT). e_k is the real I-path statistic at symbol rate; the
* one-sided error PSD (0..Rs/2 cyc/sym) is spread across the FULL occupied
* band +-3.0005 MHz = (Rs/4)(1+beta), the psi Nyquist edge -- the same band
* the blue signal trace fills -- with the midpoint (f_cyc = 0.25, the carrier
* LO-leak line) landing at 0 MHz. Normalised so the curve's mean equals
* -snr_raw (consistent with the SNR raw readout), with the real distortion
* structure (leak spike, ISI tilt) riding on top. */
#define ERR_NPTS 256
#define ERR_BAND_MHZ 3.5f /* display half-span (covers the +-3.0 band + roll-off) */
struct pr12_result {
int ok; /* field sync found and demod ran */
int di, d2, el_it, segoff, spacing, nsym;
float mu, mu_el, ratio, snr, snr_raw, theta, tau_cmp, seg_ratio, line_db;
float err_f0, err_df, err_db[ERR_NPTS];
int ms_corr, ms_el, ms_trim, ms_far, ms_dem;
};
/*
* Error power spectrum in the SAME channel-baseband frame as the blue signal
* PSD, so the two curves line up bin-for-bin. Driven by the POST-EQUALIZER
* statistic (`eqstat` = the 11-tap DD equalizer output, the one whose decision
* SNR is the published snr_db), NOT the raw statistic. From it, per symbol:
* d_k = slice(eqstat_k) (decision)
* e_k = eqstat_k - d_k (post-EQ decision error)
* Both the ideal and the error are re-modulated back into the channel baseband
* xhat(t) = sum_k (d_k+1.25)(-j)^k psi(t-t_k) (t_k = di + k/sps)
* ehat(t) = sum_k e_k (-j)^k psi(t-t_k)
* and Welch-FFT'd. out = 10log10(|FFT ehat|^2 / inband-flat(|FFT xhat|^2)),
* i.e. the residual-error density relative to the signal flat density -- the
* same "dB rel in-band flat density" axis as the blue trace. Band edges land
* at +-3.0 where the signal rolls off; the total under the curve is -snr_db
* (the equalized MER), not -snr_raw. */
#define ERR_REC 65536 /* samples reconstructed / analysed */
#define ERR_BLK 8192 /* Welch block (reuses the field-sync FFT size = FFTN) */
static float xh_re[NSAMP], xh_im[NSAMP]; /* reconstruction scratch, 2 MB */
static double sig_acc[ERR_BLK], er_acc[ERR_BLK];
static const float MJR[4] = {1.0f, 0.0f, -1.0f, 0.0f}; /* Re((-j)^k) */
static const float MJI[4] = {0.0f, -1.0f, 0.0f, 1.0f}; /* Im((-j)^k) */
/* Re-modulate a per-symbol amplitude into the channel baseband xh[] over
* [base-24, base+ERR_REC+24) using psi (the real SRRC, Nyquist Rs/4, beta
* 0.1152 -- the pulse the RTL shaper vsb_preq_srrc emits), at 10 MSPS.
* mode 0 (ideal): a = d_k + 1.25 (decision + pilot)
* mode 1 (error): a = eqstat_k - d_k (post-EQ decision error) */
static void err_reconstruct(const float *eqstat, int di, int k_lo, int nsym,
int base, int mode)
{
const float inv_sps = 1539.0f / 1430.0f;
int m, n;
for (n = base - 24; n < base + ERR_REC + 24; n++) { xh_re[n] = 0.0f; xh_im[n] = 0.0f; }
for (m = 0; m < nsym; m++) {
int kk = (k_lo + m) & 3;
float d = slice1(eqstat[m]);
float a = mode ? (eqstat[m] - d) : (d + PILOT_DC);
float br = a * MJR[kk], bi = a * MJI[kk];
float pos = (float)di + (float)(k_lo + m) * SPS;
int n0 = (int)pos, dn;
if (n0 < base - 24 || n0 > base + ERR_REC + 24) continue;
for (dn = -19; dn <= 19; dn++) {
int nn = n0 + dn;
float p;
if (nn < base - 24 || nn >= base + ERR_REC + 24) continue;
p = psi_lut(((float)nn - pos) * inv_sps);
xh_re[nn] += br * p; xh_im[nn] += bi * p;
}
}
}
static void err_welch(int base, int nblk, double *acc)
{
int b, k;
for (k = 0; k < ERR_BLK; k++) acc[k] = 0.0;
for (b = 0; b < nblk; b++) {
const float *hr_ = xh_re + base + b * ERR_BLK;
const float *hi_ = xh_im + base + b * ERR_BLK;
for (k = 0; k < ERR_BLK; k++) {
float w = 0.5f - 0.5f * cosf(2.0f * (float)M_PI * k / (ERR_BLK - 1));
fft_re[k] = hr_[k] * w; fft_im[k] = hi_[k] * w;
}
fft_c(fft_re, fft_im, 0);
for (k = 0; k < ERR_BLK; k++)
acc[k] += (double)fft_re[k] * fft_re[k] + (double)fft_im[k] * fft_im[k];
}
}
/*
* Error signal spectrum matching the equalized MER (snr_db). Reshape the
* post-EQ decisions to the ideal baseband and the post-EQ decision error
* (eqstat_k - d_k) to an error baseband -- both via the SRRC, both band-limited
* to the signal band, exactly "reshaped decisions minus the equalized IQ".
* out = 10log10(|FFT error|^2 / inband-flat(|FFT ideal|^2)) -- the residual
* density relative to the signal flat density, on the same channel-frame axis
* as the blue trace. Its integral is -snr_db (the equalized MER, e.g. -45),
* because it is built from the equalizer's own decision error, not the raw IQ. */
static void err_spectrum(const float *eqstat, int di, int k_lo, int nsym,
float snr_db_eq, float *f0, float *df, float *out)
{
int base, k, j, nblk, nflat = 0;
double flat = 0.0;
*f0 = -ERR_BAND_MHZ;
*df = (2.0f * ERR_BAND_MHZ) / (float)(ERR_NPTS - 1);
for (j = 0; j < ERR_NPTS; j++) out[j] = -99.0f;
if (nsym < 4 * ERR_BLK / 3) return;
base = di + (int)((float)k_lo * SPS) + 32;
if (base < 32) base = 32;
if (base + ERR_REC + 32 > NSAMP) base = NSAMP - ERR_REC - 32;
if (base < 32) return;
nblk = ERR_REC / ERR_BLK;
err_reconstruct(eqstat, di, k_lo, nsym, base, 0); /* ideal -> flat ref */
err_welch(base, nblk, sig_acc);
{ float binhz = 1.0e7f / ERR_BLK;
for (k = 0; k < ERR_BLK; k++) {
float f = ((k < ERR_BLK / 2) ? k : k - ERR_BLK) * binhz;
if (f > -2.5e6f && f < 2.5e6f) { flat += sig_acc[k]; nflat++; }
}
if (nflat) flat /= nflat;
}
if (!(flat > 0.0)) return;
err_reconstruct(eqstat, di, k_lo, nsym, base, 1); /* post-EQ error */
err_welch(base, nblk, er_acc);
for (j = 0; j < ERR_NPTS; j++) {
float fmhz = *f0 + (float)j * (*df);
int kb = (int)(fmhz * 1.0e6f / (1.0e7f / ERR_BLK) + (fmhz >= 0 ? 0.5f : -0.5f));
double p; int kk, cnt = 0;
if (kb < 0) kb += ERR_BLK;
p = 0.0;
for (kk = kb - 1; kk <= kb + 1; kk++) {
int idx = ((kk % ERR_BLK) + ERR_BLK) % ERR_BLK;
p += er_acc[idx]; cnt++;
}
p = (cnt > 0) ? p / cnt : flat * 1e-9;
if (!(p > 0.0)) p = flat * 1e-9;
out[j] = 10.0f * log10f((float)(p / flat));
}
/* Anchor the curve's power integral to -snr_db (the equalized MER): the
* reconstruction's absolute scale drifts a few dB from residual leak / edge
* effects, so rescale the whole trace by a constant so that
* 10log10(mean over the occupied band of 10^(out/10)) == -snr_db. Shape
* (where the error concentrates) is preserved; the total now matches the
* SNR the demod reports. */
if (snr_db_eq > 0.0f && snr_db_eq < 90.0f) {
double acc = 0.0; int nb = 0;
for (j = 0; j < ERR_NPTS; j++) {
float fmhz = *f0 + (float)j * (*df);
if (fmhz > -3.0f && fmhz < 3.0f) { acc += pow(10.0, out[j] / 10.0); nb++; }
}
if (nb > 0 && acc > 0.0) {
float cur = 10.0f * log10f((float)(acc / nb)); /* current integral dB */
float shift = -snr_db_eq - cur;
for (j = 0; j < ERR_NPTS; j++) out[j] += shift;
}
}
}
/*
* Decision-directed cancellation of a narrowband interferer at +Rs/4 in the
* c domain (channel-centre leakage -- e.g. TX LO leak -- lands there after
* the j^k derotation; its real part stamps the exact cos(pi*l/2) texture on
* the error autocorrelation). e_k = r_k - slice(r_k); the complex envelope
* W_k/2 = LP(e_k * (-j)^(k0+k)) over LC_LEN symbols (+-17.9 kHz, best in the
* PC sweep -- harmless on clean signals, +0.02 dB on the synthetic control);
* subtract 2*Re{LP * j^(k0+k)} and let the caller re-slice.
* Returns the removed line power in dB relative to Sum(d^2) (diagnostic).
*/
#define LC_LEN 301
static float lc_er[NSYM_MAX], lc_ei[NSYM_MAX];
static float line_cancel(float *r, float *q, int n, int k0)
{
double sr = 0.0, si = 0.0, removed = 0.0, sig = 0.0;
int k, half = LC_LEN / 2;
if (n < 4 * LC_LEN) return 0.0f;
for (k = 0; k < n; k++) {
float e = r[k] - slice1(r[k]);
int kk = (k0 + k) & 3;
/* e * (-j)^(k0+k): (-j)^kk = JR[kk] - j*JI[kk] */
lc_er[k] = e * JR[kk];
lc_ei[k] = -e * JI[kk];
}
/* centre-aligned running boxcar over [k-half, k+half], clamped at ends */
{ int lo = 0, hi = 0;
for (k = 0; k < n; k++) {
int wlo = k - half, whi = k + half + 1, kk = (k0 + k) & 3, cnt;
float wr, wi, l, d;
if (wlo < 0) wlo = 0;
if (whi > n) whi = n;
while (hi < whi) { sr += lc_er[hi]; si += lc_ei[hi]; hi++; }
while (lo < wlo) { sr -= lc_er[lo]; si -= lc_ei[lo]; lo++; }
cnt = hi - lo;
wr = (float)(sr / cnt);
wi = (float)(si / cnt);
/* complex line sample 2*(wr + j wi)*j^(k0+k): subtract its real
* part from the decision statistic and its imaginary part from
* the (normalised) quadrature so the equalizer sees a clean c */
l = 2.0f * (wr * JR[kk] - wi * JI[kk]);
if (q) q[k] -= 2.0f * (wr * JI[kk] + wi * JR[kk]);
d = slice1(r[k]);
sig += (double)d * d;
r[k] -= l;
removed += (double)l * l;
}
}
return (removed > 0.0 && sig > 0.0)
? 10.0f * log10f((float)(removed / sig)) : -99.0f;
}
/*
* T-spaced 11-tap complex LS equalizer, decision-directed, retrained per
* window (22 real unknowns; normal equations + Gaussian elimination). After
* the line canceller the residual floor (~-39 dB) is even-lag LINEAR ISI
* (observation-path ripple); this recovers it to ~43 dB (PC, half/half
* held-out). Real ATSC receivers/analyzers measure MER post-equalizer.
* Trains on every 2nd symbol; overfit bias with 22 dof over 70k samples is
* < 0.001 dB. Output written to out[] (edges copied through), returns SNR.
*/
#define EQ_M 5
#define EQ_NT (2 * EQ_M + 1)
#define EQ_NU (2 * EQ_NT)
static float eq_snr(const float *cr, const float *cq, int n, float *out)
{
static double xtx[EQ_NU][EQ_NU], xty[EQ_NU];
double w[EQ_NU], sd = 0.0, se = 0.0;
int k, i, j;
if (n < 100 * EQ_NT) return -99.0f;
memset(xtx, 0, sizeof(xtx));
memset(xty, 0, sizeof(xty));
for (k = EQ_M; k < n - EQ_M; k += 2) { /* stride-2 training */
double v[EQ_NU], d = slice1(cr[k]);
for (j = 0; j < EQ_NT; j++) {
v[2 * j] = cr[k - (j - EQ_M)];
v[2 * j + 1] = -cq[k - (j - EQ_M)];
}
for (i = 0; i < EQ_NU; i++) {
xty[i] += v[i] * d;
for (j = i; j < EQ_NU; j++) xtx[i][j] += v[i] * v[j];
}
}
for (i = 0; i < EQ_NU; i++) /* symmetrise + ridge */
for (j = 0; j < i; j++) xtx[i][j] = xtx[j][i];
for (i = 0; i < EQ_NU; i++) xtx[i][i] += 1e-6 * xtx[i][i];
/* Gaussian elimination with partial pivoting */
for (i = 0; i < EQ_NU; i++) {
int p = i;
double f;
for (j = i + 1; j < EQ_NU; j++)
if (fabs(xtx[j][i]) > fabs(xtx[p][i])) p = j;
if (fabs(xtx[p][i]) < 1e-12) return -99.0f;
if (p != i) {
for (j = 0; j < EQ_NU; j++) { f = xtx[i][j]; xtx[i][j] = xtx[p][j]; xtx[p][j] = f; }
f = xty[i]; xty[i] = xty[p]; xty[p] = f;
}
for (j = i + 1; j < EQ_NU; j++) {
f = xtx[j][i] / xtx[i][i];
if (f != 0.0) {
int m;
for (m = i; m < EQ_NU; m++) xtx[j][m] -= f * xtx[i][m];
xty[j] -= f * xty[i];
}
}
}
for (i = EQ_NU - 1; i >= 0; i--) {
double acc = xty[i];
for (j = i + 1; j < EQ_NU; j++) acc -= xtx[i][j] * w[j];
w[i] = acc / xtx[i][i];
}
for (k = 0; k < EQ_M; k++) out[k] = cr[k];
for (k = n - EQ_M; k < n; k++) out[k] = cr[k];
for (k = EQ_M; k < n - EQ_M; k++) {
float acc = 0.0f, d, e;
for (j = 0; j < EQ_NT; j++)
acc += (float)w[2 * j] * cr[k - (j - EQ_M)]
- (float)w[2 * j + 1] * cq[k - (j - EQ_M)];
out[k] = acc;
d = slice1(acc);
e = acc - d;
sd += (double)d * d;
se += (double)e * e;
}
return (se > 0.0) ? 10.0f * log10f((float)(sd / se)) : 99.0f;
}
static void pr12_run(struct pr12_result *R, float tau_old)
{
struct timespec t0, t1, t2, t3, t4, t5;
int m0, d0, di, it, k_lo, k_hi, nsym, off = 0, d2m;
float ratio, mu_el, mu, th, dc, scale, snr_raw, snr;
double dd;
memset(R, 0, sizeof(*R));
R->d2 = -1;
if (!pr12_ready) pr12_init();
now(&t0);
m0 = fsync_corr_c(&ratio);
now(&t1);
R->ratio = ratio;
R->ms_corr = ms(t0, t1);
if (m0 < 0 || ratio < PR12_RATIO_THR) return; /* no field sync: old chain only */
d2m = pr12_second_peak(m0, &R->spacing);
if (d2m >= 0) R->d2 = d2m + PR12_REF_PAD;
d0 = m0 + PR12_REF_PAD;
mu_el = early_late_c(d0, &di, &it);
now(&t2);
R->ms_el = ms(t1, t2);
k_lo = (int)ceilf((float)(GUARD - di) / SPS);
k_hi = (int)((float)(NSAMP - GUARD - di) / SPS);
mu = snr_trim_c(di, mu_el, k_lo);
now(&t3);
R->ms_trim = ms(t2, t3);
farrow_apply(mu);
now(&t4);
R->ms_far = ms(t3, t4);
nsym = k_hi - k_lo;
if (nsym > NSYM_USE) nsym = NSYM_USE;
if (nsym < 8 * SEG_LEN) return;
if (!resample_syms(xfi, xfq, di, k_lo, nsym, 0)) return;
th = theta_of(nsym);
apply_theta(nsym, th);
snr_raw = slice_snr(ci, nsym, &dc, &scale);
{ float inv = 1.0f / scale; /* quadrature to same scale */
int i2;
for (i2 = 0; i2 < nsym; i2++) cq[i2] *= inv;
}
gain_track(ci, nsym);
R->line_db = line_cancel(ci, cq, nsym, k_lo); /* narrowband +Rs/4 canceller */
{ float s_eq = eq_snr(ci, cq, nsym, lc_er); /* 11-tap DD equalizer -> lc_er */
const float *eqstat;
if (s_eq > -90.0f) {
snr = s_eq;
eqstat = lc_er; /* equalized statistic */
} else {
double sd = 0.0, se = 0.0;
int i;
for (i = 0; i < nsym; i++) {
float d = slice1(ci[i]);
sd += (double)d * d;
se += (double)(ci[i] - d) * (ci[i] - d);
}
snr = (se > 0.0) ? 10.0f * log10f((float)(sd / se)) : 99.0f;
eqstat = ci; /* EQ failed: fall back to tracked ci */
}
/* error spectrum matching the equalized MER: reshaped decisions minus
* the equalized signal = the post-EQ decision error, band-limited to
* the signal band, integral = -snr_db. */
/* DISABLED on request (2026-07-25): yellow error trace off — uncomment
* the call below (and the "err" JSON block + the HTML draw block) to
* restore. */
/* err_spectrum(eqstat, di, k_lo, nsym, snr, &R->err_f0, &R->err_df, R->err_db); */
(void)eqstat;
}
now(&t5);
R->ms_dem = ms(t4, t5);
R->ok = 1;
R->di = di;
R->mu = mu;
R->mu_el = mu_el;
R->el_it = it;
R->snr = snr;
R->snr_raw = snr_raw;
R->theta = th;
R->nsym = nsym;
R->seg_ratio = seg_sync(ci, nsym, &off);
R->segoff = off;
/* lattice distance to the old chain's tau (both describe the same symbol
* grid; they must agree modulo one symbol period = SPS samples) */
dd = fmod((double)di + (double)mu - (double)tau_old, (double)SPS);
if (dd < 0.0) dd += (double)SPS;
if (dd > 0.5 * (double)SPS) dd = (double)SPS - dd;
R->tau_cmp = (float)dd;
}
/* ============================================================= entry point */
void iq_vsb_analyse(const unsigned char *win, int wid)
{
@@ -582,6 +1200,9 @@ void iq_vsb_analyse(const unsigned char *win, int wid)
static char json[64 * 1024];
float th, dc, scale, snr, snr_raw, gjit = 0.0f, qi = 0.0f, coff, ratio, eye_w, minh, tau;
float snr_head, snr_head_raw;
int sync_head;
struct pr12_result pr;
int q;
double e = 0.0, s1 = 0.0, lsum[8], lsq[8];
int i, j, nsym, k0, off = 0, pos, sync, eye_open = 0, lcnt[8];
@@ -613,7 +1234,7 @@ void iq_vsb_analyse(const unsigned char *win, int wid)
nsym = (int)((NSAMP - 2 * GUARD) / SPS) - 20;
if (nsym > NSYM_USE) nsym = NSYM_USE;
if (!resample_syms(k0, nsym, q)) return;
if (!resample_syms(xi, xq, 0, k0, nsym, q)) return;
th = theta_of(nsym);
apply_theta(nsym, th);
@@ -712,42 +1333,83 @@ void iq_vsb_analyse(const unsigned char *win, int wid)
}
eye_w = (float)eye_open * 2.0f / (float)EYE_COLS; /* in UI */
/* PR12 chain runs last: its trim probes and gain_track reuse ci[]/gtrk[],
* which the old chain (incl. build_eye) must be completely done with. */
pr12_run(&pr, tau);
/* Task12 stage-2 switch: the headline SNR (web tsnr + spectrum-page card)
* comes from the field-sync chain whenever it locked; the find_tau chain
* is the fallback and still drives the eye diagram. */
{ int sync_fs = pr.ok && pr.seg_ratio > 10.0f && pr.snr > 5.0f;
if (sync_fs) { snr_head = pr.snr; snr_head_raw = pr.snr_raw; sync_head = 1; }
else { snr_head = snr; snr_head_raw = snr_raw; sync_head = sync; }
}
pos = snprintf(json, sizeof(json),
"{\"ts\":%ld,\"wid\":%d,\"nsym\":%d,\"sync\":%s,"
"\"snr_db\":%.2f,\"snr_raw_db\":%.2f,\"gain_jitter_pct\":%.2f,"
"\"snr_db\":%.2f,\"snr_raw_db\":%.2f,\"snr_legacy_db\":%.2f,\"gain_jitter_pct\":%.2f,"
"\"q_isi_db\":%.2f,\"tau\":%.4f,\"theta_deg\":%.2f,"
"\"carrier_off_hz\":%.3f,\"segsync_ratio\":%.1f,\"segsync_off\":%d,"
"\"fs\":{\"sync\":%s,\"d0\":%d,\"mu\":%.4f,\"mu_el\":%.4f,\"ratio\":%.1f,"
"\"snr_db\":%.2f,\"snr_raw_db\":%.2f,\"segoff\":%d,\"tau_cmp\":%.2e,"
"\"d2\":%d,\"spacing\":%d,\"el_it\":%d,\"line_db\":%.1f},"
"\"levels\":[%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f],"
"\"sigma\":[%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f],"
"\"eye_h\":[%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f],\"eye_w_ui\":%.3f,"
"\"eye\":{\"cols\":%d,\"rows\":%d,\"ui\":2.0,\"amp\":%.1f,\"log\":true,\"data\":\"%s\"}}\n",
(long)time(NULL), wid, nsym, sync ? "true" : "false",
snr, snr_raw, 100.0f * gjit, qi, tau, th * 180.0f / (float)M_PI, coff, ratio, off,
"\"eye\":{\"cols\":%d,\"rows\":%d,\"ui\":2.0,\"amp\":%.1f,\"log\":true,\"data\":\"%s\"}",
(long)time(NULL), wid, nsym, sync_head ? "true" : "false",
snr_head, snr_head_raw, snr, 100.0f * gjit, qi, tau, th * 180.0f / (float)M_PI, coff, ratio, off,
pr.ok ? "true" : "false", pr.di, pr.mu, pr.mu_el, pr.ratio,
pr.snr, pr.snr_raw, pr.segoff, pr.tau_cmp,
pr.d2, pr.spacing, pr.el_it, pr.line_db,
lev[0], lev[1], lev[2], lev[3], lev[4], lev[5], lev[6], lev[7],
sig[0], sig[1], sig[2], sig[3], sig[4], sig[5], sig[6], sig[7],
eyeh[0], eyeh[1], eyeh[2], eyeh[3], eyeh[4], eyeh[5], eyeh[6], eye_w,
EYE_COLS, EYE_ROWS, EYE_AMP, b64buf);
/* append the error power spectrum (yellow web trace), then close the root */
/* DISABLED on request (2026-07-25): drop the leading `0 &&` to restore */
if (0 && pr.ok && pos > 0 && pos < (int)sizeof(json) - 2048) {
int jj;
pos += snprintf(json + pos, sizeof(json) - pos,
",\"err\":{\"f0\":%.4f,\"df\":%.5f,\"db\":[",
pr.err_f0, pr.err_df);
for (jj = 0; jj < ERR_NPTS; jj++)
pos += snprintf(json + pos, sizeof(json) - pos, "%s%.1f",
jj ? "," : "", pr.err_db[jj]);
pos += snprintf(json + pos, sizeof(json) - pos, "]}}\n");
} else {
pos += snprintf(json + pos, sizeof(json) - pos, "}\n");
}
{ FILE *f = fopen("/tmp/iq_eye.json.tmp", "w");
if (f) { fwrite(json, 1, pos, f); fclose(f);
rename("/tmp/iq_eye.json.tmp", "/tmp/iq_eye.json"); }
}
if (sync) {
if (sync_head) {
pthread_mutex_lock(&snr_mtx);
snr_db_pub = snr; snr_stamp = time(NULL);
snr_db_pub = snr_head; snr_stamp = time(NULL);
pthread_mutex_unlock(&snr_mtx);
}
minh = eyeh[0];
for (j = 1; j < 7; j++) if (eyeh[j] < minh) minh = eyeh[j];
if(iq_vsb_log_allowed())
{
printf("[iq_vsb] win %d: SNR %.2f dB (raw %.2f, ORx AGC jitter %.2f%%) tau %.4f theta %+.1f deg coff %+.3f Hz "
"segsync %.1f@%d eye_w %.3f UI min_eye_h %.2f %s "
"[ms: unpack %d tau %d demod %d stats %d eye %d total %d]\n",
wid, snr, snr_raw, 100.0f * gjit, tau, th * 180.0f / (float)M_PI, coff, ratio, off, eye_w,
minh, sync ? "SYNC" : "NOSYNC",
ms(t0, t1), ms(t1, t2), ms(t2, t3), ms(t3, t4), ms(t4, t5), ms(t0, t5));
}
printf("[iq_vsb] win %d: SNR %.2f dB (raw %.2f, ORx AGC jitter %.2f%%) tau %.4f theta %+.1f deg coff %+.3f Hz "
"segsync %.1f@%d eye_w %.3f UI min_eye_h %.2f %s "
"[ms: unpack %d tau %d demod %d stats %d eye %d total %d]\n",
wid, snr, snr_raw, 100.0f * gjit, tau, th * 180.0f / (float)M_PI, coff, ratio, off, eye_w,
minh, sync ? "SYNC" : "NOSYNC",
ms(t0, t1), ms(t1, t2), ms(t2, t3), ms(t3, t4), ms(t4, t5), ms(t0, t5));
if (pr.ok)
printf("[pr12] win %d: FS SNR %.2f dB (raw %.2f, line %.1f dB) d0 %d mu %+.4f (el %+.4f, %d it) "
"ratio %.1f segsync %.1f@%d tau_cmp %.1e d2 %d spacing %d PUBLISH=%s "
"[ms: corr %d el %d trim %d farrow %d demod %d]\n",
wid, pr.snr, pr.snr_raw, pr.line_db, pr.di, pr.mu, pr.mu_el, pr.el_it,
pr.ratio, pr.seg_ratio, pr.segoff, pr.tau_cmp, pr.d2, pr.spacing,
(snr_head == pr.snr) ? "FS" : "LEGACY",
pr.ms_corr, pr.ms_el, pr.ms_trim, pr.ms_far, pr.ms_dem);
else
printf("[pr12] win %d: no field sync (ratio %.1f < %.1f), LEGACY fallback\n",
wid, pr.ratio, PR12_RATIO_THR);
}