/** \file * @brief member function of the UI_Constructor class * creates the waterfall spectrum */ #include "JS8_UI/mainwindow.h" /** * Emulation of the Fortran 'flat1' subroutine. */ void flat1(float const *const savg, int const iz, int const nsmo, float *const slin) { constexpr int x_size = 8192; constexpr int nstep = 20; constexpr int nh = nstep / 2; // Define bounds for smoothing int const ia = nsmo / 2 + 1; int const ib = iz - nsmo / 2 - 1; std::vector x(x_size, 0.0f); // Smooth savg using median percentiles auto const rank = std::clamp(static_cast(std::round(0.5f * nsmo)), 0, nsmo - 1); for (int i = ia; i <= ib; i += nstep) { auto const data = &savg[i - nsmo / 2]; auto temp = std::vector(data, data + nsmo); std::nth_element(temp.begin(), temp.begin() + rank, temp.end()); x[i] = temp[rank]; std::fill(x.begin() + (i - nh), x.begin() + (i + nh), x[i]); } // Extend smoothed values to boundaries std::fill(x.begin(), x.begin() + ia, x[ia]); std::fill(x.begin() + ib + 1, x.begin() + iz, x[ib]); // Compute scaling factor float x0 = 0.001f * *std::max_element(x.begin() + iz / 10, x.begin() + (9 * iz) / 10); // Normalize savg to compute slin for (int i = 0; i < iz; ++i) slin[i] = savg[i] / (x[i] + x0); } /** Emulation of the Fortran 'smo' subroutine. However, doesn't copy the data * back from b to a; rather, a is input and, b is output. Since we invariably * call this twice, we can just swap the order of the arrays to achieve the * same result without the extra two copy operations. */ void smo(float const *const a, float *const b, int const npts, int const nadd) { auto const nh = nadd / 2; // Smooth the array for (int i = nh; i < npts - nh; ++i) { float sum = 0.0f; for (int j = -nh; j <= nh; ++j) { sum += a[i + j]; } b[i] = sum; } // Set edges to zero for (int i = 0; i < nh; ++i) b[i] = 0.0f; // Zero out leading edge for (int i = npts - nh; i < npts; ++i) b[i] = 0.0f; // Zero out trailing edge } //-------------------------------------------------------------- dataSink() void UI_Constructor::dataSink(qint64 frames) { constexpr int NMAX = JS8_NTMAX * 12000; constexpr int nfft3 = 16384; constexpr std::array nch = {1, 2, 4, 9, 18, 36, 72}; // symspec global vars static int ja = 0; static int k0 = 999999999; static WF::SPlot ssum = {}; static WF::SPlot s = {}; int k(frames); if (k0 == 999999999) { m_ihsym = int((float)frames / (float)JS8_NSPS) * 2; ja = k; k0 = k; } // qCDebug(mainwindow_js8) << "k" << k << "k0" << k0 << "delta" << k-k0; // Get power, spectrum, and ihsym int const nsmo = m_wideGraph->smoothYellow() - 1; // make sure the ssum global is reset every period cycle static int lastCycle = -1; int const cycle = JS8::Submode::computeCycleForDecode(m_nSubMode, k); if (cycle != lastCycle) { qCDebug(decoder_js8) << "period loop, resetting ssum"; ssum.fill(0.0f); } lastCycle = cycle; // cap ihsym based on the period max m_ihsym = m_ihsym % (m_TRperiod * JS8_RX_SAMPLE_RATE / JS8_NSPS * 2); int const jstep = JS8_NSPS / 2; if (k >= 2048 && k <= NMAX) { if (k < k0) { // Start a new data block ja = 0; ssum.fill(0.0f); m_ihsym = 0; std::fill(std::begin(dec_data.d2) + k, std::end(dec_data.d2), 0); } float gain = pow(10.0f, 0.1f * m_inGain); float sq = 0.0f; float pxmax = 0.0f; for (int i = k0; i < k; ++i) { float x1 = dec_data.d2[i]; pxmax = std::max(pxmax, fabs(x1)); sq += x1 * x1; } m_px = sq > 0.0f ? 10.0f * log10(sq / (k - k0)) : 0.0f; m_pxmax = pxmax > 0.0f ? 20.0f * log10(pxmax) : 0.0f; k0 = k; ja += jstep; /** Perform real to complex FFT; note that the Fortran code performed * this operation in the four2a subroutine, which contains a cache * of plans. However, since the FFTW3 library itself caches, this * resulted in double caching, so as an experiment we're allowing * the library to handle caching. * * Note that the FFTW library requires all calls but for the plan * execution, i.e., fftwf_execute(), to be serialized. The library * doesn't require prescriptive serialization, anything will do so * long as the result is one thread at a time. * * Providing room for an extra complex value, i.e., a pair of floats, * real and imaginary parts, allows us to use the same buffer for the * FFT input and output. * * While the memory for the FFT can come from anywhere, if we ask the * library for it, it'll guarantee that it's aligned for use of SIMD * instructions, which will in turn allow it to use them. */ fftwf_complex *fftw_complex; float *fftw_real; fftwf_plan fftw_plan; { std::lock_guard lock(fftw_mutex); fftw_complex = fftwf_alloc_complex(nfft3 / 2 + 1); if (!fftw_complex) { throw std::runtime_error("Failed to allocate FFT data"); } fftw_real = reinterpret_cast(fftw_complex); fftw_plan = fftwf_plan_dft_r2c_1d(nfft3, fftw_real, fftw_complex, FFTW_ESTIMATE_PATIENT); if (!fftw_plan) { fftwf_free(fftw_complex); throw std::runtime_error("Failed to create FFT plan"); } } // Copy data and apply the window, then execute the FFT. for (int i = 0; i < nfft3; ++i) { if (int j = ja + i - nfft3; j >= 0 && j < NMAX) fftw_real[i] = 0.1f * dec_data.d2[j]; } ++m_ihsym; fftwf_execute(fftw_plan); // Process the resulting spectrum. m_df3 = 12000.0f / nfft3; auto const iz = std::min(JS8_NSMAX, static_cast(5000.0f / m_df3)); auto const cx = reinterpret_cast *>(fftw_complex); auto const fac = std::pow(1.0f / nfft3, 2.0f); for (int i = 0; i < iz; ++i) { auto const sx = fac * std::norm(cx[i]); ssum[i] += sx; s[i] = 1000.0f * gain * sx; } // We're now done with the plan and the FFT storage. Even though // the plan is being 'destroyed' here, the library will cache it // at its discretion. As above, the calls must be serialized. { std::lock_guard lock(fftw_mutex); fftwf_destroy_plan(fftw_plan); fftwf_free(fftw_complex); } // Update average spectra. for (int i = 0; i < iz; ++i) specData.savg[i] = ssum[i] / m_ihsym; if (m_ihsym % 10 == 0) { auto const mode4 = nch[nsmo]; auto const nsmo = 4 * std::min(10 * mode4, 150); flat1(specData.savg, iz, nsmo, specData.slin); if (mode4 >= 2) { std::array tmp; smo(specData.slin, tmp.data(), iz, mode4); smo(tmp.data(), specData.slin, iz, mode4); } std::fill(std::begin(specData.slin), std::begin(specData.slin) + 250, 0.0f); auto const ia = static_cast(500.0 / m_df3); auto const ib = static_cast(2700.0 / m_df3); auto const smin = *std::min_element(std::begin(specData.slin) + ia, std::begin(specData.slin) + ib); auto const smax = *std::max_element(std::begin(specData.slin), std::begin(specData.slin) + iz); auto const scale = (smax > smin) ? 50.0f / (smax - smin) : 0.0f; for (auto &val : specData.slin) val = std::max(0.0f, scale * (val - smin)); } } else if (k < 2048) m_ihsym = 0; // make sure ja is equal to k so if we jump ahead in the buffer, everything // resolves correctly ja = k; if (m_ihsym <= 0) return; if (ui) ui->signal_meter_widget->setValue(m_px, m_pxmax); // Update thermometer if (m_monitoring) m_wideGraph->dataSink(s, m_df3); decode(k); }