mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
rename remaining files that didn't match their class definitions relocate the explicit UI_Constructor member function to JS8_MainWindow relocate the processBufferedActivity member function to JS8_MainWindow format affected files with LLVM clang-format Set default APRS inbound relay to off Fix layout of General->Networking & Autoreply tab in Settings
267 lines
8.5 KiB
C++
267 lines
8.5 KiB
C++
#include "JS8_UI/mainwindow.h"
|
|
|
|
/** \file
|
|
* @brief member function of the UI_Constructor class
|
|
* creates the waterfall spectrum
|
|
*/
|
|
|
|
/**
|
|
* 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<float> x(x_size, 0.0f);
|
|
|
|
// Smooth savg using median percentiles
|
|
|
|
auto const rank =
|
|
std::clamp(static_cast<int>(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<float>(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<std::mutex> 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<float *>(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<int>(5000.0f / m_df3));
|
|
auto const cx = reinterpret_cast<std::complex<float> *>(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<std::mutex> 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<float, JS8_NSMAX> 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<int>(500.0 / m_df3);
|
|
auto const ib = static_cast<int>(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);
|
|
}
|