// This file is part of LeanSDR Copyright (C) 2016-2019 . // See the toplevel README for more information. // // 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 3 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, see . #ifndef LEANSDR_SDR_H #define LEANSDR_SDR_H #include "leansdr/dsp.h" #include "leansdr/math.h" namespace leansdr { // Abbreviations for floating-point types typedef float f32; typedef complex cu8; typedef complex cs8; typedef complex cu16; typedef complex cs16; typedef complex cf32; ////////////////////////////////////////////////////////////////////// // SDR blocks ////////////////////////////////////////////////////////////////////// // AUTO-NOTCH FILTER // Periodically detects the [nslots] strongest peaks with a FFT, // removes them with a first-order filter. template struct auto_notch : runnable { int decimation; float k; auto_notch(scheduler *sch, pipebuf> &_in, pipebuf> &_out, int _nslots, T _agc_rms_setpoint) : runnable(sch, "auto_notch"), decimation(1024 * 4096), k(0.002), // k(0.01) fft(4096), in(_in), out(_out, fft.n), nslots(_nslots), phase(0), gain(1), agc_rms_setpoint(_agc_rms_setpoint) { __slots = new slot[nslots]; for (int s = 0; s < nslots; ++s) { __slots[s].i = -1; __slots[s].expj = new complex[fft.n]; } } ~auto_notch() { delete[] __slots; } void run() { while (in.readable() >= fft.n && out.writable() >= fft.n) { phase += fft.n; if (phase >= decimation) { phase -= decimation; detect(); } process(); in.read(fft.n); out.written(fft.n); } } void detect() { complex *pin = in.rd(); complex *data = new complex[fft.n]; float m0 = 0, m2 = 0; for (int i = 0; i < fft.n; ++i) { data[i].re = pin[i].re; data[i].im = pin[i].im; m2 += (float)pin[i].re * pin[i].re + (float)pin[i].im * pin[i].im; if (gen_abs(pin[i].re) > m0) m0 = gen_abs(pin[i].re); if (gen_abs(pin[i].im) > m0) m0 = gen_abs(pin[i].im); } if (agc_rms_setpoint && m2) { float rms = gen_sqrt(m2 / fft.n); if (sch->debug) fprintf(stderr, "(pow %f max %f)", rms, m0); float new_gain = agc_rms_setpoint / rms; gain = gain * 0.9 + new_gain * 0.1; } fft.inplace(data, true); float *amp = new float[fft.n]; for (int i = 0; i < fft.n; ++i) amp[i] = hypotf(data[i].re, data[i].im); for (slot *s = __slots; s < __slots + nslots; ++s) { int iamax = 0; for (int i = 0; i < fft.n; ++i) if (amp[i] > amp[iamax]) iamax = i; if (iamax != s->i) { if (sch->debug) fprintf(stderr, "%s: slot %d new peak %d -> %d\n", name, (int)(s - __slots), s->i, iamax); s->i = iamax; s->estim.re = 0; s->estim.im = 0; s->estt = 0; for (int i = 0; i < fft.n; ++i) { float a = 2 * M_PI * s->i * i / fft.n; s->expj[i].re = cosf(a); s->expj[i].im = sinf(a); } } amp[iamax] = 0; if (iamax - 1 >= 0) amp[iamax - 1] = 0; if (iamax + 1 < fft.n) amp[iamax + 1] = 0; } delete[] amp; delete[] data; } void process() { complex *pin = in.rd(), *pend = pin + fft.n, *pout = out.wr(); for (slot *s = __slots; s < __slots + nslots; ++s) s->ej = s->expj; for (; pin < pend; ++pin, ++pout) { complex out = *pin; // TODO Optimize for nslots==1 ? for (slot *s = __slots; s < __slots + nslots; ++s->ej, ++s) { complex bb(pin->re * s->ej->re + pin->im * s->ej->im, -pin->re * s->ej->im + pin->im * s->ej->re); s->estim.re = bb.re * k + s->estim.re * (1 - k); s->estim.im = bb.im * k + s->estim.im * (1 - k); complex sub( s->estim.re * s->ej->re - s->estim.im * s->ej->im, s->estim.re * s->ej->im + s->estim.im * s->ej->re); out.re -= sub.re; out.im -= sub.im; } pout->re = gain * out.re; pout->im = gain * out.im; } } private: cfft_engine fft; pipereader> in; pipewriter> out; int nslots; struct slot { int i; complex estim; complex *expj, *ej; int estt; }; slot *__slots; int phase; float gain; T agc_rms_setpoint; }; // SIGNAL STRENGTH ESTIMATOR // Outputs RMS values. template struct ss_estimator : runnable { unsigned long window_size; // Samples per estimation unsigned long decimation; // Output rate ss_estimator(scheduler *sch, pipebuf> &_in, pipebuf &_out) : runnable(sch, "SS estimator"), window_size(1024), decimation(1024), in(_in), out(_out), phase(0) { } void run() { while (in.readable() >= window_size && out.writable() >= 1) { phase += window_size; if (phase >= decimation) { phase -= decimation; complex *p = in.rd(), *pend = p + window_size; float s = 0; for (; p < pend; ++p) s += (float)p->re * p->re + (float)p->im * p->im; out.write(sqrtf(s / window_size)); } in.read(window_size); } } private: pipereader> in; pipewriter out; unsigned long phase; }; template struct ss_amp_estimator : runnable { unsigned long window_size; // Samples per estimation unsigned long decimation; // Output rate ss_amp_estimator(scheduler *sch, pipebuf> &_in, pipebuf &_out_ss, pipebuf &_out_ampmin, pipebuf &_out_ampmax) : runnable(sch, "SS estimator"), window_size(1024), decimation(1024), in(_in), out_ss(_out_ss), out_ampmin(_out_ampmin), out_ampmax(_out_ampmax), phase(0) { } void run() { while (in.readable() >= window_size && out_ss.writable() >= 1 && out_ampmin.writable() >= 1 && out_ampmax.writable() >= 1) { phase += window_size; if (phase >= decimation) { phase -= decimation; complex *p = in.rd(), *pend = p + window_size; float s2 = 0; float amin = 1e38, amax = 0; for (; p < pend; ++p) { float mag2 = (float)p->re * p->re + (float)p->im * p->im; s2 += mag2; float mag = sqrtf(mag2); if (mag < amin) amin = mag; if (mag > amax) amax = mag; } out_ss.write(sqrtf(s2 / window_size)); out_ampmin.write(amin); out_ampmax.write(amax); } in.read(window_size); } } private: pipereader> in; pipewriter out_ss, out_ampmin, out_ampmax; unsigned long phase; }; // AGC template struct simple_agc : runnable { float out_rms; // Desired RMS output power float bw; // Bandwidth float estimated; // Input power static const int chunk_size = 128; simple_agc(scheduler *sch, pipebuf> &_in, pipebuf> &_out) : runnable(sch, "AGC"), out_rms(1), bw(0.001), estimated(0), in(_in), out(_out, chunk_size) { } private: pipereader> in; pipewriter> out; void run() { while (in.readable() >= chunk_size && out.writable() >= chunk_size) { complex *pin = in.rd(), *pend = pin + chunk_size; float amp2 = 0; for (; pin < pend; ++pin) amp2 += pin->re * pin->re + pin->im * pin->im; amp2 /= chunk_size; if (!estimated) estimated = amp2; estimated = estimated * (1 - bw) + amp2 * bw; float gain = estimated ? out_rms / sqrtf(estimated) : 0; pin = in.rd(); complex *pout = out.wr(); float bwcomp = 1 - bw; for (; pin < pend; ++pin, ++pout) { pout->re = pin->re * gain; pout->im = pin->im * gain; } in.read(chunk_size); out.written(chunk_size); } } }; // simple_agc typedef uint16_t u_angle; // [0,2PI[ in 65536 steps typedef int16_t s_angle; // [-PI,PI[ in 65536 steps // GENERIC CONSTELLATION DECODING BY LOOK-UP TABLE. // Metrics and phase errors are pre-computed on a RxR grid. // R must be a power of 2. // Up to 256 symbols. struct softsymbol { // TBD obsolete int16_t cost; // For Viterbi with TBM=int16_t uint8_t symbol; }; // Target RMS amplitude for AGC //const float cstln_amp = 73; // Best for 32APSK 9/10 //const float cstln_amp = 90; // Best for QPSK //const float cstln_amp = 64; // Best for BPSK //const float cstln_amp = 75; // Best for BPSK at 45° const float cstln_amp = 75; // Trade-off // A struct that temporarily holds all the info we precompute for the LUT. struct full_ss { uint8_t nearest; // Index of nearest in constellation uint16_t dists2[256]; // Squared distances float p[8]; // 0..1 probability of bits being 1 }; // Options for soft-symbols. // These functions are overloaded to keep cstln_lut generic: // to_softsymb(const full_ss *fss, SOFTSYMB *ss) // softsymb_harden(SOFTSYMB *ss) { // softsymb_to_dump(const SOFTSYMB &ss, int bit) To grey 0..255 // For LUT initialization only. Performance is not critical. // Hard decision soft-symbols. // Value is the symbol index, 0..255. typedef uint8_t hard_ss; void to_softsymb(const full_ss *fss, hard_ss *ss); void softsymb_harden(hard_ss *ss); uint8_t softsymb_to_dump(const hard_ss &ss, int bit); // Euclidian QPSK soft-symbols. // Additive metric suitable for Viterbi. // Backward-compatible with simplified Viterbi (TBD remove) struct eucl_ss { static const int MAX_SYMBOLS = 4; uint16_t dists2[MAX_SYMBOLS]; uint16_t discr2; // 2nd_nearest - nearest uint8_t nearest; }; void to_softsymb(const full_ss *fss, eucl_ss *ss); void softsymb_harden(eucl_ss *ss); uint8_t softsymb_to_dump(const eucl_ss &ss, int bit); // Log-Likelihood Ratios soft-symbols typedef int8_t llr_t; // log(p(0)/p(1)), clip -127=1 +127=0 inline bool llr_harden(llr_t v) { return v & 128; } struct llr_ss { llr_t bits[8]; // Up to 8 bit considered independent }; void to_softsymb(const full_ss *fss, llr_ss *ss); void softsymb_harden(llr_ss *ss); uint8_t softsymb_to_dump(const llr_ss &ss, int bit); struct cstln_base { enum predef { BPSK, // DVB-S2 (and DVB-S variant) QPSK, // DVB-S PSK8, APSK16, APSK32, // DVB-S2 APSK64E, // DVB-S2X QAM16, QAM64, QAM256, // For experimentation only COUNT }; static const char *names[]; float amp_max; // Max amplitude. 1 for PSK, 0 if not applicable. complex *symbols; int nsymbols; int nrotations; }; // cstln_base template struct cstln_lut : cstln_base { cstln_lut(cstln_base::predef type, float mer = 10, float gamma1 = 0, float gamma2 = 0, float gamma3 = 0) { switch (type) { case BPSK: amp_max = 1; nrotations = 2; nsymbols = 2; symbols = new complex[nsymbols]; #if 0 // BPSK at 0° symbols[0] = polar(1, 2, 0); symbols[1] = polar(1, 2, 1); #else // BPSK at 45° symbols[0] = polar(1, 8, 1); symbols[1] = polar(1, 8, 5); #endif make_lut_from_symbols(mer); break; case QPSK: amp_max = 1; // EN 300 421, section 4.5 Baseband shaping and modulation // EN 302 307, section 5.4.1 nrotations = 4; nsymbols = 4; symbols = new complex[nsymbols]; symbols[0] = polar(1, 4, 0.5); symbols[1] = polar(1, 4, 3.5); symbols[2] = polar(1, 4, 1.5); symbols[3] = polar(1, 4, 2.5); make_lut_from_symbols(mer); break; case PSK8: amp_max = 1; // EN 302 307, section 5.4.2 nrotations = 8; nsymbols = 8; symbols = new complex[nsymbols]; symbols[0] = polar(1, 8, 1); symbols[1] = polar(1, 8, 0); symbols[2] = polar(1, 8, 4); symbols[3] = polar(1, 8, 5); symbols[4] = polar(1, 8, 2); symbols[5] = polar(1, 8, 7); symbols[6] = polar(1, 8, 3); symbols[7] = polar(1, 8, 6); make_lut_from_symbols(mer); break; case APSK16: { // Default gamma for non-DVB-S2 applications. if (!gamma1) gamma1 = 2.57; // EN 302 307, section 5.4.3 float r1 = sqrtf(4 / (1 + 3 * gamma1 * gamma1)); float r2 = gamma1 * r1; amp_max = r2; nrotations = 4; nsymbols = 16; symbols = new complex[nsymbols]; symbols[0] = polar(r2, 12, 1.5); symbols[1] = polar(r2, 12, 10.5); symbols[2] = polar(r2, 12, 4.5); symbols[3] = polar(r2, 12, 7.5); symbols[4] = polar(r2, 12, 0.5); symbols[5] = polar(r2, 12, 11.5); symbols[6] = polar(r2, 12, 5.5); symbols[7] = polar(r2, 12, 6.5); symbols[8] = polar(r2, 12, 2.5); symbols[9] = polar(r2, 12, 9.5); symbols[10] = polar(r2, 12, 3.5); symbols[11] = polar(r2, 12, 8.5); symbols[12] = polar(r1, 4, 0.5); symbols[13] = polar(r1, 4, 3.5); symbols[14] = polar(r1, 4, 1.5); symbols[15] = polar(r1, 4, 2.5); make_lut_from_symbols(mer); break; } case APSK32: { // Default gammas for non-DVB-S2 applications. if (!gamma1) gamma1 = 2.53; if (!gamma2) gamma2 = 4.30; // EN 302 307, section 5.4.3 float r1 = sqrtf( 8 / (1 + 3 * gamma1 * gamma1 + 4 * gamma2 * gamma2)); float r2 = gamma1 * r1; float r3 = gamma2 * r1; amp_max = r3; nrotations = 4; nsymbols = 32; symbols = new complex[nsymbols]; symbols[0] = polar(r2, 12, 1.5); symbols[1] = polar(r2, 12, 2.5); symbols[2] = polar(r2, 12, 10.5); symbols[3] = polar(r2, 12, 9.5); symbols[4] = polar(r2, 12, 4.5); symbols[5] = polar(r2, 12, 3.5); symbols[6] = polar(r2, 12, 7.5); symbols[7] = polar(r2, 12, 8.5); symbols[8] = polar(r3, 16, 1); symbols[9] = polar(r3, 16, 3); symbols[10] = polar(r3, 16, 14); symbols[11] = polar(r3, 16, 12); symbols[12] = polar(r3, 16, 6); symbols[13] = polar(r3, 16, 4); symbols[14] = polar(r3, 16, 9); symbols[15] = polar(r3, 16, 11); symbols[16] = polar(r2, 12, 0.5); symbols[17] = polar(r1, 4, 0.5); symbols[18] = polar(r2, 12, 11.5); symbols[19] = polar(r1, 4, 3.5); symbols[20] = polar(r2, 12, 5.5); symbols[21] = polar(r1, 4, 1.5); symbols[22] = polar(r2, 12, 6.5); symbols[23] = polar(r1, 4, 2.5); symbols[24] = polar(r3, 16, 0); symbols[25] = polar(r3, 16, 2); symbols[26] = polar(r3, 16, 15); symbols[27] = polar(r3, 16, 13); symbols[28] = polar(r3, 16, 7); symbols[29] = polar(r3, 16, 5); symbols[30] = polar(r3, 16, 8); symbols[31] = polar(r3, 16, 10); make_lut_from_symbols(mer); break; } case APSK64E: { // Default gammas for non-DVB-S2 applications. if (!gamma1) gamma1 = 2.4; if (!gamma2) gamma2 = 4.3; if (!gamma3) gamma3 = 7.0; // EN 302 307-2, section 5.4.5, Table 13e float r1 = sqrtf( 64 / (4 + 12 * gamma1 * gamma1 + 20 * gamma2 * gamma2 + 28 * gamma3 * gamma3)); float r2 = gamma1 * r1; float r3 = gamma2 * r1; float r4 = gamma3 * r1; amp_max = r4; nrotations = 4; nsymbols = 64; symbols = new complex[nsymbols]; polar2(0, r4, 1.0 / 4, 7.0 / 4, 3.0 / 4, 5.0 / 4); polar2(4, r4, 13.0 / 28, 43.0 / 28, 15.0 / 28, 41.0 / 28); polar2(8, r4, 1.0 / 28, 55.0 / 28, 27.0 / 28, 29.0 / 28); polar2(12, r1, 1.0 / 4, 7.0 / 4, 3.0 / 4, 5.0 / 4); polar2(16, r4, 9.0 / 28, 47.0 / 28, 19.0 / 28, 37.0 / 28); polar2(20, r4, 11.0 / 28, 45.0 / 28, 17.0 / 28, 39.0 / 28); polar2(24, r3, 1.0 / 20, 39.0 / 20, 19.0 / 20, 21.0 / 20); polar2(28, r2, 1.0 / 12, 23.0 / 12, 11.0 / 12, 13.0 / 12); polar2(32, r4, 5.0 / 28, 51.0 / 28, 23.0 / 28, 33.0 / 28); polar2(36, r3, 9.0 / 20, 31.0 / 20, 11.0 / 20, 29.0 / 20); polar2(40, r4, 3.0 / 28, 53.0 / 28, 25.0 / 28, 31.0 / 28); polar2(44, r2, 5.0 / 12, 19.0 / 12, 7.0 / 12, 17.0 / 12); polar2(48, r3, 1.0 / 4, 7.0 / 4, 3.0 / 4, 5.0 / 4); polar2(52, r3, 7.0 / 20, 33.0 / 20, 13.0 / 20, 27.0 / 20); polar2(56, r3, 3.0 / 20, 37.0 / 20, 17.0 / 20, 23.0 / 20); polar2(60, r2, 1.0 / 4, 7.0 / 4, 3.0 / 4, 5.0 / 4); make_lut_from_symbols(mer); break; } case QAM16: amp_max = 0; make_qam(16); break; case QAM64: amp_max = 1; make_qam(64); break; case QAM256: amp_max = 1; make_qam(256); break; default: fail("Constellation not implemented"); } } struct result { SOFTSYMB ss; s_angle phase_error; uint8_t symbol; // Nearest symbol, useful for C&T recovery }; inline result *lookup(float I, float Q) { // Handling of overflows beyond the lookup table: // - For BPSK/QPSK/8PSK we only care about the phase, // so the following is harmless and improves locking at low SNR. // - For amplitude modulations this is not appropriate. // However, if there is enough noise to cause overflow, // demodulation would probably fail anyway. // // Comment-out for better throughput at high SNR. #if 1 while (I < -128 || I > 127 || Q < -128 || Q > 127) { I *= 0.5; Q *= 0.5; } #endif return &lut[(u8)(s8)I][(u8)(s8)Q]; } inline result *lookup(int I, int Q) { // Ignore wrapping modulo 256 return &lut[(u8)I][(u8)Q]; } private: complex polar(float r, int n, float i) { float a = i * 2 * M_PI / n; return complex(r * cosf(a) * cstln_amp, r * sinf(a) * cstln_amp); } // Helper function for some constellation tables void polar2(int i, float r, float a0, float a1, float a2, float a3) { float a[] = {a0, a1, a2, a3}; for (int j = 0; j < 4; ++j) { float phi = a[j] * M_PI; symbols[i + j] = complex(r * cosf(phi) * cstln_amp, r * sinf(phi) * cstln_amp); } } void make_qam(int n) { nrotations = 4; nsymbols = n; symbols = new complex[nsymbols]; int m = sqrtl(n); float scale; { // Average power in first quadrant with unit grid int q = m / 2; float avgpower = 2 * (q * 0.25 + (q - 1) * q / 2 + (q - 1) * q * (2 * q - 1) / 6) / q; scale = 1.0 / sqrtf(avgpower); } // Arbitrary mapping int s = 0; for (int x = 0; x < m; ++x) { for (int y = 0; y < m; ++y) { float I = x - (float)(m - 1) / 2; float Q = y - (float)(m - 1) / 2; symbols[s].re = I * scale * cstln_amp; symbols[s].im = Q * scale * cstln_amp; ++s; } } make_lut_from_symbols(20); // TBD } result lut[R][R]; void make_lut_from_symbols(float mer) { // Note: Excessively low values of MER will break 16APSK and 32APSK. float sigma = cstln_amp * exp10f(-mer / 20); // Precomputed values. // Shared scope so that we don't have to reset dists2[nsymbols..] to -1. struct full_ss fss; for (int s = 0; s < 256; ++s) fss.dists2[s] = -1; for (int I = -R / 2; I < R / 2; ++I) { for (int Q = -R / 2; Q < R / 2; ++Q) { // Nearest symbol fss.nearest = 0; fss.dists2[0] = 65535; // Conditional probabilities: // Sum likelyhoods from all candidate symbols. // // P(TX[b]==B | RX==IQ) = // sum(S=0..nsymbols-1, P(TX[b]==B | RX==IQ && TXs==S)) // // P(TX[b] == B | RX==IQ && TX==S) = // P(TX[b]==B && RX==IQ && TX==S) / P(RX==IQ && TX==S) float probs[8][2]; memset(probs, 0, sizeof(probs)); for (int s = 0; s < nsymbols; ++s) { float d2 = ((I - symbols[s].re) * (I - symbols[s].re) + (Q - symbols[s].im) * (Q - symbols[s].im)); if (d2 < fss.dists2[fss.nearest]) fss.nearest = s; fss.dists2[s] = d2; float p = expf(-d2 / (2 * sigma * sigma)) / (sqrtf(2 * M_PI) * sigma); for (int bit = 0; bit < 8; ++bit) { probs[bit][(s >> bit) & 1] += p; } } // Normalize for (int b = 0; b < 8; ++b) { float p = probs[b][1] / (probs[b][0] + probs[b][1]); // Avoid trouble when sigma is unrealistically low. if (!isnormal(p)) p = 0; fss.p[b] = p; } result *pr = &lut[I & (R - 1)][Q & (R - 1)]; to_softsymb(&fss, &pr->ss); // Always record nearest symbol and phase error for C&T. pr->symbol = fss.nearest; float ph_symbol = atan2f(symbols[pr->symbol].im, symbols[pr->symbol].re); float ph_err = atan2f(Q, I) - ph_symbol; pr->phase_error = (int32_t)(ph_err * 65536 / (2 * M_PI)); // Mod 65536 } } } public: void dump(FILE *f) { int bps = log2(nsymbols); fprintf(f, "P5\n%d %d\n255\n", R, R * (bps + 1)); for (int bit = 0; bit < bps + 1; ++bit) { for (int Q = R / 2 - 1; Q >= -R / 2; --Q) { for (int I = -R / 2; I < R / 2; ++I) { result *pr = &lut[I & (R - 1)][Q & (R - 1)]; uint8_t v; if (bit < bps) v = softsymb_to_dump(pr->ss, bit); else v = 128 + pr->phase_error / 64; // Highlight the constellation symbols. for (int s = 0; s < nsymbols; ++s) { if (symbols[s].re == I && symbols[s].im == Q) v ^= 128; } fputc(v, f); } } } } // Convert soft metric to Hamming distance void harden() { for (int i = 0; i < R; ++i) { for (int q = 0; q < R; ++q) softsymb_harden(&lut[i][q].ss); } } }; // cstln_lut // SAMPLER INTERFACE FOR CSTLN_RECEIVER template struct sampler_interface { virtual ~sampler_interface() { } virtual complex interp(const complex *pin, float mu, float phase) = 0; virtual void update_freq(float freqw, int period = 1) { (void) freqw; (void) period; } // 65536 = 1 Hz virtual int readahead() = 0; }; // NEAREST-SAMPLE SAMPLER FOR CSTLN_RECEIVER // Suitable for bandpass-filtered, oversampled signals only template struct nearest_sampler : sampler_interface { int readahead() { return 0; } complex interp(const complex *pin, float mu, float phase) { (void) mu; return pin[0] * trig.expi(-phase); } private: trig16 trig; }; // nearest_sampler // LINEAR SAMPLER FOR CSTLN_RECEIVER template struct linear_sampler : sampler_interface { int readahead() { return 1; } complex interp(const complex *pin, float mu, float phase) { // Derotate pin[0] and pin[1] complex s0 = pin[0] * trig.expi(-phase); complex s1 = pin[1] * trig.expi(-(phase + freqw)); // Interpolate linearly return s0 * (1 - mu) + s1 * mu; } void update_freq(float _freqw, int period = 1) { (void) period; freqw = _freqw; } private: trig16 trig; float freqw; }; // linear_sampler // FIR SAMPLER FOR CSTLN_RECEIVER template struct fir_sampler : sampler_interface { fir_sampler(int _ncoeffs, Tc *_coeffs, int _subsampling = 1) : ncoeffs(_ncoeffs), coeffs(_coeffs), subsampling(_subsampling), shifted_coeffs(new complex[ncoeffs]), update_freq_phase(0) { do_update_freq(0); // In case application never calls update_freq() } int readahead() { return ncoeffs - 1; } complex interp(const complex *pin, float mu, float phase) { // Apply FIR filter with subsampling complex acc(0, 0); complex *pc = shifted_coeffs + (int)((1 - mu) * subsampling); complex *pcend = shifted_coeffs + ncoeffs; if (subsampling == 1) { // Special case for heavily oversampled signals, // where filtering is expensive. // gcc-4.9.2 can vectorize this form with NEON on ARM. while (pc < pcend) acc += (*pc++) * (*pin++); } else { // Not vectorized because the coefficients are not // guaranteed to be contiguous in memory. for (; pc < pcend; pc += subsampling, ++pin) acc += (*pc) * (*pin); } // Derotate return trig.expi(-phase) * acc; } void update_freq(float freqw, int period) { // Throttling: Update one coeff per 16 processed samples, // to keep the overhead of freq tracking below about 10%. update_freq_phase -= period; if (update_freq_phase <= 0) { update_freq_phase = ncoeffs * 16; do_update_freq(freqw); } } private: void do_update_freq(float freqw) { float f = freqw / subsampling; for (int i = 0; i < ncoeffs; ++i) shifted_coeffs[i] = trig.expi(-f * (i - ncoeffs / 2)) * coeffs[i]; } trig16 trig; int ncoeffs; Tc *coeffs; int subsampling; cf32 *shifted_coeffs; int update_freq_phase; }; // fir_sampler // CONSTELLATION RECEIVER // Linear interpolation: good enough for 1.2 samples/symbol, // but higher oversampling is recommended. template struct cstln_receiver : runnable { sampler_interface *sampler; cstln_lut *cstln; unsigned long meas_decimation; // Measurement rate float omega, min_omega, max_omega; // Samples per symbol float freqw, min_freqw, max_freqw; // Freq offs (65536 = 1 Hz) float pll_adjustment; bool allow_drift; // Follow carrier beyond safe limits static const unsigned int chunk_size = 128; float kest; cstln_receiver(scheduler *sch, sampler_interface *_sampler, pipebuf> &_in, pipebuf &_out, pipebuf *_freq_out = NULL, pipebuf *_ss_out = NULL, pipebuf *_mer_out = NULL, pipebuf *_cstln_out = NULL) : runnable(sch, "Constellation receiver"), sampler(_sampler), cstln(NULL), meas_decimation(1048576), pll_adjustment(1.0), allow_drift(false), kest(0.01), in(_in), out(_out, chunk_size), est_insp(cstln_amp * cstln_amp), agc_gain(1), mu(0), phase(0), est_sp(0), est_ep(0), meas_count(0) { set_omega(1); set_freq(0); freq_out = _freq_out ? new pipewriter(*_freq_out) : NULL; ss_out = _ss_out ? new pipewriter(*_ss_out) : NULL; mer_out = _mer_out ? new pipewriter(*_mer_out) : NULL; cstln_out = _cstln_out ? new pipewriter(*_cstln_out) : NULL; memset(hist, 0, sizeof(hist)); } void set_omega(float _omega, float tol = 10e-6) { omega = _omega; min_omega = omega * (1 - tol); max_omega = omega * (1 + tol); update_freq_limits(); } void set_freq(float freq) { freqw = freq * 65536; update_freq_limits(); refresh_freq_tap(); } void set_allow_drift(bool d) { allow_drift = d; } void update_freq_limits() { // Prevent PLL from crossing +-SR/n/2 and locking at +-SR/n. int n = 4; if (cstln) { switch (cstln->nsymbols) { case 2: n = 2; break; // BPSK case 4: n = 4; break; // QPSK case 8: n = 8; break; // 8PSK case 16: n = 12; break; // 16APSK case 32: n = 16; break; // 32APSK default: n = 4; break; } } min_freqw = freqw - 65536 / max_omega / n / 2; max_freqw = freqw + 65536 / max_omega / n / 2; } void run() { if (!cstln) fail("constellation not set"); // Magic constants that work with the qa recordings. float freq_alpha = 0.04; float freq_beta = 0.0012 / omega * pll_adjustment; float gain_mu = 0.02 / (cstln_amp * cstln_amp) * 2; int max_meas = chunk_size / meas_decimation + 1; // Large margin on output_size because mu adjustments // can lead to more than chunk_size/min_omega symbols. while (in.readable() >= chunk_size + sampler->readahead() && out.writable() >= chunk_size && (!freq_out || freq_out->writable() >= max_meas) && (!ss_out || ss_out->writable() >= max_meas) && (!mer_out || mer_out->writable() >= max_meas) && (!cstln_out || cstln_out->writable() >= max_meas)) { sampler->update_freq(freqw, chunk_size); complex *pin = in.rd(), *pin0 = pin, *pend = pin + chunk_size; SOFTSYMB *pout = out.wr(), *pout0 = pout; // These are scoped outside the loop for SS and MER estimation. complex sg{0.0f, 0.0f}; // Symbol before AGC; complex s; // For MER estimation and constellation viewer complex *cstln_point = NULL; while (pin < pend) { // Here mu is the time of the next symbol counted from 0 at pin. if (mu < 1) { // Here 0<=mu<1 is the fractional time of the next symbol // between pin and pin+1. sg = sampler->interp(pin, mu, phase + mu * freqw); s = sg * agc_gain; // Constellation look-up typename cstln_lut::result *cr = cstln->lookup(s.re, s.im); *pout = cr->ss; ++pout; // PLL phase += cr->phase_error * freq_alpha; freqw += cr->phase_error * freq_beta; // Modified Mueller and Müller // mu[k]=real((c[k]-c[k-2])*conj(p[k-1])-(p[k]-p[k-2])*conj(c[k-1])) // =dot(c[k]-c[k-2],p[k-1]) - dot(p[k]-p[k-2],c[k-1]) // p = received signals // c = decisions (constellation points) hist[2] = hist[1]; hist[1] = hist[0]; hist[0].p.re = s.re; hist[0].p.im = s.im; cstln_point = &cstln->symbols[cr->symbol]; hist[0].c.re = cstln_point->re; hist[0].c.im = cstln_point->im; float muerr = ((hist[0].p.re - hist[2].p.re) * hist[1].c.re + (hist[0].p.im - hist[2].p.im) * hist[1].c.im) - ((hist[0].c.re - hist[2].c.re) * hist[1].p.re + (hist[0].c.im - hist[2].c.im) * hist[1].p.im); float mucorr = muerr * gain_mu; const float max_mucorr = 0.1; // TBD Optimize out statically if (mucorr < -max_mucorr) mucorr = -max_mucorr; if (mucorr > max_mucorr) mucorr = max_mucorr; mu += mucorr; mu += omega; // Next symbol time; } // mu<1 // Next sample ++pin; --mu; phase += freqw; } // chunk_size in.read(pin - pin0); out.written(pout - pout0); // Normalize phase so that it never exceeds 32 bits. // Max freqw is 2^31/65536/chunk_size = 256 Hz // (this may happen with leandvb --drift --decim). phase = fmodf(phase, 65536); // Rounding direction irrelevant if (cstln_point) { // Output the last interpolated PSK symbol, max once per chunk_size if (cstln_out) cstln_out->write(s); // AGC // For APSK we must do AGC on the symbols, not the whole signal. // TODO Use a better estimator at low SNR. float insp = sg.re * sg.re + sg.im * sg.im; est_insp = insp * kest + est_insp * (1 - kest); if (est_insp) agc_gain = cstln_amp / gen_sqrt(est_insp); // SS and MER complex ev(s.re - cstln_point->re, s.im - cstln_point->im); float sig_power, ev_power; if (cstln->nsymbols == 2) { // Special case for BPSK: Ignore quadrature component of noise. // TBD Projection on I axis assumes BPSK at 45° float sig_real = (cstln_point->re + cstln_point->im) * 0.707; float ev_real = (ev.re + ev.im) * 0.707; sig_power = sig_real * sig_real; ev_power = ev_real * ev_real; } else { sig_power = (int)cstln_point->re * cstln_point->re + (int)cstln_point->im * cstln_point->im; ev_power = ev.re * ev.re + ev.im * ev.im; } est_sp = sig_power * kest + est_sp * (1 - kest); est_ep = ev_power * kest + est_ep * (1 - kest); } // This is best done periodically ouside the inner loop, // but will cause non-deterministic output. if (!allow_drift) { if (freqw < min_freqw || freqw > max_freqw) freqw = (max_freqw + min_freqw) / 2; } // Output measurements refresh_freq_tap(); meas_count += pin - pin0; while (meas_count >= meas_decimation) { meas_count -= meas_decimation; if (freq_out) freq_out->write(freq_tap); if (ss_out) ss_out->write(sqrtf(est_insp)); if (mer_out) mer_out->write( est_ep ? 10 * logf(est_sp / est_ep) / logf(10) : 0); } } // Work to do } float freq_tap; void refresh_freq_tap() { freq_tap = freqw / 65536; } private: struct { complex p; // Received symbol complex c; // Matched constellation point } hist[3]; pipereader> in; pipewriter out; float est_insp, agc_gain; float mu; // PSK time expressed in clock ticks float phase; // 65536=2pi // Signal estimation float est_sp; // Estimated RMS signal power float est_ep; // Estimated RMS error vector power unsigned long meas_count; pipewriter *freq_out, *ss_out, *mer_out; pipewriter *cstln_out; }; // FAST QPSK RECEIVER // Optimized for u8 input, no AGC, uses phase information only. // Outputs hard symbols. template struct fast_qpsk_receiver : runnable { typedef u8 hardsymbol; unsigned long meas_decimation; // Measurement rate float omega, min_omega, max_omega; // Samples per symbol signed long freqw, min_freqw, max_freqw; // Freq offs (angle per sample) float pll_adjustment; bool allow_drift; // Follow carrier beyond safe limits static const unsigned int chunk_size = 128; fast_qpsk_receiver(scheduler *sch, pipebuf> &_in, pipebuf &_out, pipebuf *_freq_out = NULL, pipebuf> *_cstln_out = NULL) : runnable(sch, "Fast QPSK receiver"), meas_decimation(1048576), pll_adjustment(1.0), allow_drift(false), in(_in), out(_out, chunk_size), mu(0), phase(0), meas_count(0) { set_omega(1); set_freq(0); freq_out = _freq_out ? new pipewriter(*_freq_out) : NULL; cstln_out = _cstln_out ? new pipewriter>(*_cstln_out) : NULL; memset(hist, 0, sizeof(hist)); init_lookup_tables(); } void set_omega(float _omega, float tol = 10e-6) { omega = _omega; min_omega = omega * (1 - tol); max_omega = omega * (1 + tol); update_freq_limits(); } void set_freq(float freq) { freqw = freq * 65536; update_freq_limits(); } void update_freq_limits() { // Prevent PLL from locking at +-symbolrate/4. // TODO The +-SR/8 limit is suitable for QPSK only. min_freqw = freqw - 65536 / max_omega / 8; max_freqw = freqw + 65536 / max_omega / 8; } static const int RLUT_BITS = 8; static const int RLUT_ANGLES = 1 << RLUT_BITS; void run() { // Magic constants that work with the qa recordings. signed long freq_alpha = 0.04 * 65536; signed long freq_beta = 0.0012 * 256 * 65536 / omega * pll_adjustment; if (!freq_beta) fail("Excessive oversampling"); float gain_mu = 0.02 / (cstln_amp * cstln_amp) * 2; int max_meas = chunk_size / meas_decimation + 1; // Largin margin on output_size because mu adjustments // can lead to more than chunk_size/min_omega symbols. while (in.readable() >= chunk_size + 1 && // +1 for interpolation out.writable() >= chunk_size && (!freq_out || freq_out->writable() >= max_meas) && (!cstln_out || cstln_out->writable() >= max_meas)) { complex *pin = in.rd(), *pin0 = pin, *pend = pin + chunk_size; hardsymbol *pout = out.wr(), *pout0 = pout; cu8 s; u_angle symbol_arg = 0; // Exported for constellation viewer while (pin < pend) { // Here mu is the time of the next symbol counted from 0 at pin. if (mu < 1) { // Here 0<=mu<1 is the fractional time of the next symbol // between pin and pin+1. // Derotate and interpolate #if 0 /* Phase only (does not work) Careful with the float/signed/unsigned casts */ u_angle a0 = fast_arg(pin[0]) - phase; u_angle a1 = fast_arg(pin[1]) - (phase+freqw); s_angle da = a1 - a0; symbol_arg = a0 + (s_angle)(da*mu); s = arg_to_symbol(symbol_arg); #elif 1 // Linear by lookup-table. 1.2M on bench3bishs polar *p0 = &lut_polar[pin[0].re][pin[0].im]; u_angle a0 = (u_angle)(p0->a - phase) >> (16 - RLUT_BITS); cu8 *p0r = &lut_rect[a0][p0->r >> 1]; polar *p1 = &lut_polar[pin[1].re][pin[1].im]; u_angle a1 = (u_angle)(p1->a - (phase + freqw)) >> (16 - RLUT_BITS); cu8 *p1r = &lut_rect[a1][p1->r >> 1]; s.re = (int)(p0r->re + (p1r->re - p0r->re) * mu); s.im = (int)(p0r->im + (p1r->im - p0r->im) * mu); symbol_arg = fast_arg(s); #else // Linear floating-point, for reference float a0 = -(int)phase * M_PI / 32768; float cosa0 = cosf(a0), sina0 = sinf(a0); complex p0r(((float)pin[0].re - 128) * cosa0 - ((float)pin[0].im - 128) * sina0, ((float)pin[0].re - 128) * sina0 + ((float)pin[0].im - 128) * cosa0); float a1 = -(int)(phase + freqw) * M_PI / 32768; float cosa1 = cosf(a1), sina1 = sinf(a1); complex p1r(((float)pin[1].re - 128) * cosa1 - ((float)pin[1].im - 128) * sina1, ((float)pin[1].re - 128) * sina1 + ((float)pin[1].im - 128) * cosa1); s.re = (int)(128 + p0r.re + (p1r.re - p0r.re) * mu); s.im = (int)(128 + p0r.im + (p1r.im - p0r.im) * mu); symbol_arg = fast_arg(s); #endif int quadrant = symbol_arg >> 14; static unsigned char quadrant_to_symbol[4] = {0, 2, 3, 1}; *pout = quadrant_to_symbol[quadrant]; ++pout; // PLL s_angle phase_error = (s_angle)(symbol_arg & 16383) - 8192; phase += (phase_error * freq_alpha + 32768) >> 16; freqw += (phase_error * freq_beta + 32768 * 256) >> 24; // Modified Mueller and Müller // mu[k]=real((c[k]-c[k-2])*conj(p[k-1])-(p[k]-p[k-2])*conj(c[k-1])) // =dot(c[k]-c[k-2],p[k-1]) - dot(p[k]-p[k-2],c[k-1]) // p = received signals // c = decisions (constellation points) hist[2] = hist[1]; hist[1] = hist[0]; #define HIST_FLOAT 0 #if HIST_FLOAT hist[0].p.re = (float)s.re - 128; hist[0].p.im = (float)s.im - 128; cu8 cp = arg_to_symbol((symbol_arg & 49152) + 8192); hist[0].c.re = (float)cp.re - 128; hist[0].c.im = (float)cp.im - 128; float muerr = ((hist[0].p.re - hist[2].p.re) * hist[1].c.re + (hist[0].p.im - hist[2].p.im) * hist[1].c.im) - ((hist[0].c.re - hist[2].c.re) * hist[1].p.re + (hist[0].c.im - hist[2].c.im) * hist[1].p.im); #else hist[0].p = s; hist[0].c = arg_to_symbol((symbol_arg & 49152) + 8192); int muerr = ((signed char)(hist[0].p.re - hist[2].p.re) * ((int)hist[1].c.re - 128) + (signed char)(hist[0].p.im - hist[2].p.im) * ((int)hist[1].c.im - 128)) - ((signed char)(hist[0].c.re - hist[2].c.re) * ((int)hist[1].p.re - 128) + (signed char)(hist[0].c.im - hist[2].c.im) * ((int)hist[1].p.im - 128)); #endif float mucorr = muerr * gain_mu; const float max_mucorr = 0.1; // TBD Optimize out statically if (mucorr < -max_mucorr) mucorr = -max_mucorr; if (mucorr > max_mucorr) mucorr = max_mucorr; mu += mucorr; mu += omega; // Next symbol time; } // mu<1 // Next sample ++pin; --mu; phase += freqw; } // chunk_size in.read(pin - pin0); out.written(pout - pout0); if (symbol_arg && cstln_out) // Output the last interpolated PSK symbol, max once per chunk_size cstln_out->write(s); // This is best done periodically ouside the inner loop, // but will cause non-deterministic output. if (!allow_drift) { if (freqw < min_freqw || freqw > max_freqw) freqw = (max_freqw + min_freqw) / 2; } // Output measurements meas_count += pin - pin0; while (meas_count >= meas_decimation) { meas_count -= meas_decimation; if (freq_out) freq_out->write((float)freqw / 65536); } } // Work to do } private: struct polar { u_angle a; unsigned char r; } lut_polar[256][256]; u_angle fast_arg(const cu8 &c) { // TBD read cu8 as u16 index, same endianness as in init() return lut_polar[c.re][c.im].a; } cu8 lut_rect[RLUT_ANGLES][256]; cu8 lut_sincos[65536]; cu8 arg_to_symbol(u_angle a) { return lut_sincos[a]; } void init_lookup_tables() { for (int i = 0; i < 256; ++i) { for (int q = 0; q < 256; ++q) { // Don't cast float to unsigned directly lut_polar[i][q].a = (s_angle)(atan2f(q - 128, i - 128) * 65536 / (2 * M_PI)); lut_polar[i][q].r = (int)hypotf(i - 128, q - 128); } } for (unsigned long a = 0; a < 65536; ++a) { float f = 2 * M_PI * a / 65536; lut_sincos[a].re = 128 + cstln_amp * cosf(f); lut_sincos[a].im = 128 + cstln_amp * sinf(f); } for (int a = 0; a < RLUT_ANGLES; ++a) { for (int r = 0; r < 256; ++r) { lut_rect[a][r].re = (int)(128 + r * cos(2 * M_PI * a / RLUT_ANGLES)); lut_rect[a][r].im = (int)(128 + r * sin(2 * M_PI * a / RLUT_ANGLES)); } } } struct { #if HIST_FLOAT complex p; // Received symbol complex c; // Matched constellation point #else cu8 p; // Received symbol cu8 c; // Matched constellation point #endif } hist[3]; pipereader in; pipewriter out; float mu; // PSK time expressed in clock ticks. TBD fixed point. u_angle phase; unsigned long meas_count; pipewriter *freq_out, *mer_out; pipewriter *cstln_out; }; // fast_qpsk_receiver // CONSTELLATION TRANSMITTER // Maps symbols to I/Q points. template struct cstln_transmitter : runnable { cstln_lut *cstln; cstln_transmitter(scheduler *sch, pipebuf &_in, pipebuf> &_out) : runnable(sch, "cstln_transmitter"), in(_in), out(_out), cstln(0) { } void run() { if (!cstln) fail("constellation not set"); int count = min(in.readable(), out.writable()); u8 *pin = in.rd(), *pend = pin + count; complex *pout = out.wr(); for (; pin < pend; ++pin, ++pout) { complex *cp = &cstln->symbols[*pin]; pout->re = Zout + cp->re; pout->im = Zout + cp->im; } in.read(count); out.written(count); } private: pipereader in; pipewriter> out; }; // cstln_transmitter // FREQUENCY SHIFTER // Resolution is sample_freq/65536. template struct rotator : runnable { rotator(scheduler *sch, pipebuf> &_in, pipebuf> &_out, float freq) : runnable(sch, "rotator"), in(_in), out(_out), index(0) { int ifreq = freq * 65536; if (sch->debug) fprintf(stderr, "Rotate: req=%f real=%f\n", freq, ifreq / 65536.0); for (int i = 0; i < 65536; ++i) { lut_cos[i] = cosf(2 * M_PI * i * ifreq / 65536); lut_sin[i] = sinf(2 * M_PI * i * ifreq / 65536); } } void run() { unsigned long count = min(in.readable(), out.writable()); complex *pin = in.rd(), *pend = pin + count; complex *pout = out.wr(); for (; pin < pend; ++pin, ++pout, ++index) { float c = lut_cos[index]; float s = lut_sin[index]; pout->re = pin->re * c - pin->im * s; pout->im = pin->re * s + pin->im * c; } in.read(count); out.written(count); } private: pipereader> in; pipewriter> out; float lut_cos[65536]; float lut_sin[65536]; unsigned short index; // Current phase }; // rotator // SPECTRUM-BASED CNR ESTIMATOR // Assumes that the spectrum is as follows: // // ---|--noise---|-roll-off-|---carrier+noise----|-roll-off-|---noise--|--- // | (bw/2) | (bw) | (bw/2) | (bw) | (bw/2) | // // Maximum roll-off 0.5 template struct cnr_fft : runnable { cnr_fft(scheduler *sch, pipebuf> &_in, pipebuf &_out, float _bandwidth, int nfft = 4096) : runnable(sch, "cnr_fft"), bandwidth(_bandwidth), freq_tap(NULL), tap_multiplier(1), decimation(1048576), kavg(0.1), in(_in), out(_out), fft(nfft), avgpower(NULL), phase(0) { if (bandwidth > 0.25) fail("CNR estimator requires Fsampling > 4x Fsignal"); } float bandwidth; float *freq_tap, tap_multiplier; int decimation; float kavg; void run() { while (in.readable() >= fft.n && out.writable() >= 1) { phase += fft.n; if (phase >= decimation) { phase -= decimation; do_cnr(); } in.read(fft.n); } } private: void do_cnr() { float center_freq = freq_tap ? *freq_tap * tap_multiplier : 0; int icf = floor(center_freq * fft.n + 0.5); complex *data = new complex[fft.n]; memcpy(data, in.rd(), fft.n * sizeof(data[0])); fft.inplace(data, true); T *power = new T[fft.n]; for (int i = 0; i < fft.n; ++i) power[i] = data[i].re * data[i].re + data[i].im * data[i].im; if (!avgpower) { // Initialize with first spectrum avgpower = new T[fft.n]; memcpy(avgpower, power, fft.n * sizeof(avgpower[0])); } // Accumulate and low-pass filter for (int i = 0; i < fft.n; ++i) avgpower[i] = avgpower[i] * (1 - kavg) + power[i] * kavg; int bwslots = (bandwidth / 4) * fft.n; if (!bwslots) { delete[] power; delete[] data; return; } // Measure carrier+noise in center band float c2plusn2 = avgslots(icf - bwslots, icf + bwslots); // Measure noise left and right of roll-off zones float n2 = (avgslots(icf - bwslots * 4, icf - bwslots * 3) + avgslots(icf + bwslots * 3, icf + bwslots * 4)) / 2; float c2 = c2plusn2 - n2; float cnr = (c2 > 0 && n2 > 0) ? 10 * logf(c2 / n2) / logf(10) : -50; out.write(cnr); delete[] power; delete[] data; } float avgslots(int i0, int i1) { // i0 <= i1 T s = 0; for (int i = i0; i <= i1; ++i) s += avgpower[i & (fft.n - 1)]; return s / (i1 - i0 + 1); } pipereader> in; pipewriter out; cfft_engine fft; T *avgpower; int phase; }; // cnr_fft template struct spectrum : runnable { spectrum(scheduler *sch, pipebuf> &_in, pipebuf &_out) : runnable(sch, "spectrum"), decimation(1048576), kavg(0.1), decim(1), in(in), out(_out), fft(NFFT), avgpower(NULL), phase(0) { } int decimation; float kavg; int decim; void run() { while (in.readable() >= fft.n * decim && out.writable() >= 1) { phase += fft.n * decim; if (phase >= decimation) { phase -= decimation; do_spectrum(); } in.read(fft.n * decim); } } private: void do_spectrum() { complex data[fft.n]; if (decim == 1) { memcpy(data, in.rd(), fft.n * sizeof(data[0])); } else { complex *pin = in.rd(); for (int i = 0; i < fft.n; ++i, pin += decim) data[i] = *pin; } fft.inplace(data, true); float power[NFFT]; for (int i = 0; i < fft.n; ++i) power[i] = (float)data[i].re * data[i].re + (float)data[i].im * data[i].im; if (!avgpower) { // Initialize with first spectrum avgpower = new float[fft.n]; memcpy(avgpower, power, fft.n * sizeof(avgpower[0])); } // Accumulate and low-pass filter for (int i = 0; i < fft.n; ++i) avgpower[i] = avgpower[i] * (1 - kavg) + power[i] * kavg; // Reuse power[] for (int i = 0; i < fft.n / 2; ++i) { power[i] = 10 * log10f(avgpower[NFFT / 2 + i]); power[NFFT / 2 + i] = 10 * log10f(avgpower[i]); } memcpy(out.wr(), power, sizeof(power[0]) * NFFT); out.written(1); } pipereader> in; pipewriter out; cfft_engine fft; T *avgpower; int phase; }; // spectrum } // namespace leansdr #endif // LEANSDR_SDR_H