mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-08-24 15:16:36 -04:00
548 lines
17 KiB
C++
548 lines
17 KiB
C++
#include "EqualizationToolsDialog.hpp"
|
|
|
|
#include <iterator>
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <cmath>
|
|
|
|
#include <QDir>
|
|
#include <QVector>
|
|
#include <QHBoxLayout>
|
|
#include <QDialog>
|
|
#include <QDialogButtonBox>
|
|
#include <QPushButton>
|
|
#include <QFileDialog>
|
|
#include <QSettings>
|
|
|
|
#include "SettingsGroup.hpp"
|
|
#include "qcustomplot.h"
|
|
#include "pimpl_impl.hpp"
|
|
|
|
namespace
|
|
{
|
|
float constexpr PI = 3.1415927f;
|
|
char const * const title = "Equalization Tools";
|
|
size_t constexpr intervals = 144;
|
|
|
|
// plot data loaders - wraps a plot providing value_type and
|
|
// push_back so that a std::back_inserter output iterator can be
|
|
// used to load plot data
|
|
template<typename T, typename A>
|
|
struct plot_data_loader
|
|
{
|
|
public:
|
|
typedef T value_type;
|
|
|
|
// the adjust argument is a function that is passed the plot
|
|
// pointer, the graph index and a data point, it returns a
|
|
// possibly adjusted data point and can modify the graph including
|
|
// adding extra points or gaps (quiet_NaN)
|
|
plot_data_loader (QCustomPlot * plot, int graph_index, A adjust)
|
|
: plot_ {plot}
|
|
, index_ {graph_index}
|
|
, adjust_ (adjust)
|
|
{
|
|
}
|
|
|
|
// load point into graph
|
|
void push_back (value_type const& d)
|
|
{
|
|
plot_->graph (index_)->data ()->add (adjust_ (plot_, index_, d));
|
|
}
|
|
|
|
private:
|
|
QCustomPlot * plot_;
|
|
int index_;
|
|
A adjust_;
|
|
};
|
|
// helper function template to make a plot_data_loader instance
|
|
template<typename A>
|
|
auto make_plot_data_loader (QCustomPlot * plot, int index, A adjust)
|
|
-> plot_data_loader<QCPGraphData, decltype (adjust)>
|
|
{
|
|
return plot_data_loader<QCPGraphData, decltype (adjust)> {plot, index, adjust};
|
|
}
|
|
// identity adjust function when no adjustment is needed with the
|
|
// above instantiation helper
|
|
QCPGraphData adjust_identity (QCustomPlot *, int, QCPGraphData const& v) {return v;}
|
|
|
|
// a plot_data_loader adjustment function that wraps Y values of
|
|
// (-1..+1) plotting discontinuities as gaps in the graph data
|
|
auto wrap_pi = [] (QCustomPlot * plot, int index, QCPGraphData d)
|
|
{
|
|
double constexpr limit {1};
|
|
static unsigned wrap_count {0};
|
|
static double last_x {std::numeric_limits<double>::lowest ()};
|
|
|
|
d.value += 2 * limit * wrap_count;
|
|
if (d.value > limit)
|
|
{
|
|
// insert a gap in the graph
|
|
plot->graph (index)->data ()->add ({last_x + (d.key - last_x) / 2
|
|
, std::numeric_limits<double>::quiet_NaN ()});
|
|
while (d.value > limit)
|
|
{
|
|
--wrap_count;
|
|
d.value -= 2 * limit;
|
|
}
|
|
}
|
|
else if (d.value < -limit)
|
|
{
|
|
// insert a gap into the graph
|
|
plot->graph (index)->data ()->add ({last_x + (d.key - last_x) / 2
|
|
, std::numeric_limits<double>::quiet_NaN ()});
|
|
while (d.value < -limit)
|
|
{
|
|
++wrap_count;
|
|
d.value += 2 * limit;
|
|
}
|
|
}
|
|
last_x = d.key;
|
|
return d;
|
|
};
|
|
|
|
// generate points of type R from a function of type F for X in
|
|
// (-1..+1) with N intervals and function of type SX to scale X and
|
|
// of type SY to scale Y
|
|
//
|
|
// it is up to the user to call the generator sufficient times which
|
|
// is interval+1 times to reach +1
|
|
template<typename R, typename F, typename SX, typename SY>
|
|
struct graph_generator
|
|
{
|
|
public:
|
|
graph_generator (F f, size_t intervals, SX x_scaling, SY y_scaling)
|
|
: x_ {0}
|
|
, f_ (f)
|
|
, intervals_ {intervals}
|
|
, x_scaling_ (x_scaling)
|
|
, y_scaling_ (y_scaling)
|
|
{
|
|
}
|
|
|
|
R operator () ()
|
|
{
|
|
typename F::value_type x {x_++ * 2.f / intervals_ - 1.f};
|
|
return {x_scaling_ (x), y_scaling_ (f_ (x))};
|
|
}
|
|
|
|
private:
|
|
int x_;
|
|
F f_;
|
|
size_t intervals_;
|
|
SX x_scaling_;
|
|
SY y_scaling_;
|
|
};
|
|
// helper function template to make a graph_generator instance for
|
|
// QCPGraphData type points with intervals intervals
|
|
template<typename F, typename SX, typename SY>
|
|
auto make_graph_generator (F function, SX x_scaling, SY y_scaling)
|
|
-> graph_generator<QCPGraphData, F, decltype (x_scaling), decltype (y_scaling)>
|
|
{
|
|
return graph_generator<QCPGraphData, F, decltype (x_scaling), decltype (y_scaling)>
|
|
{function, intervals, x_scaling, y_scaling};
|
|
}
|
|
|
|
// template function object for a polynomial with coefficients
|
|
template<typename C>
|
|
class polynomial
|
|
{
|
|
public:
|
|
typedef typename C::value_type value_type;
|
|
|
|
explicit polynomial (C const& coefficients)
|
|
: c_ {coefficients}
|
|
{
|
|
}
|
|
|
|
value_type operator () (value_type const& x)
|
|
{
|
|
value_type y {};
|
|
for (typename C::size_type i = c_.size (); i > 0; --i)
|
|
{
|
|
y = c_[i - 1] + x * y;
|
|
}
|
|
return y;
|
|
}
|
|
|
|
private:
|
|
C c_;
|
|
};
|
|
// helper function template to instantiate a polynomial instance
|
|
template<typename C>
|
|
auto make_polynomial (C const& coefficients) -> polynomial<C>
|
|
{
|
|
return polynomial<C> (coefficients);
|
|
}
|
|
|
|
// template function object for a group delay with coefficients
|
|
template<typename C>
|
|
class group_delay
|
|
{
|
|
public:
|
|
typedef typename C::value_type value_type;
|
|
|
|
explicit group_delay (C const& coefficients)
|
|
: c_ {coefficients}
|
|
{
|
|
}
|
|
|
|
value_type operator () (value_type const& x)
|
|
{
|
|
value_type tau {};
|
|
for (typename C::size_type i = 2; i < c_.size (); ++i)
|
|
{
|
|
tau += i * c_[i] * std::pow (x, i - 1);
|
|
}
|
|
return -1 / (2 * PI) * tau;
|
|
}
|
|
|
|
private:
|
|
C c_;
|
|
};
|
|
// helper function template to instantiate a group_delay function
|
|
// object
|
|
template<typename C>
|
|
auto make_group_delay (C const& coefficients) -> group_delay<C>
|
|
{
|
|
return group_delay<C> (coefficients);
|
|
}
|
|
|
|
// handy identity function
|
|
template<typename T> T identity (T const& v) {return v;}
|
|
|
|
// a lambda that scales the X axis from normalized to (500..2500)Hz
|
|
auto freq_scaling = [] (float v) -> float {return 1500.f + 1000.f * v;};
|
|
|
|
// a lambda that scales the phase Y axis from radians to units of Pi
|
|
auto pi_scaling = [] (float v) -> float {return v / PI;};
|
|
}
|
|
|
|
class EqualizationToolsDialog::impl final
|
|
: public QDialog
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
explicit impl (EqualizationToolsDialog * self, QSettings * settings
|
|
, QDir const& data_directory, QVector<double> const& coefficients
|
|
, QWidget * parent);
|
|
~impl () {save_window_state ();}
|
|
|
|
protected:
|
|
void closeEvent (QCloseEvent * e) override
|
|
{
|
|
save_window_state ();
|
|
QDialog::closeEvent (e);
|
|
}
|
|
|
|
private:
|
|
void save_window_state ()
|
|
{
|
|
SettingsGroup g (settings_, title);
|
|
settings_->setValue ("geometry", saveGeometry ());
|
|
}
|
|
|
|
void plot_current ();
|
|
void plot_phase ();
|
|
void plot_amplitude ();
|
|
|
|
EqualizationToolsDialog * self_;
|
|
QSettings * settings_;
|
|
QDir data_directory_;
|
|
QHBoxLayout layout_;
|
|
QVector<double> current_coefficients_;
|
|
QVector<double> new_coefficients_;
|
|
unsigned amp_poly_low_;
|
|
unsigned amp_poly_high_;
|
|
QVector<double> amp_coefficients_;
|
|
QCustomPlot plot_;
|
|
QDialogButtonBox button_box_;
|
|
};
|
|
|
|
#include "EqualizationToolsDialog.moc"
|
|
|
|
EqualizationToolsDialog::EqualizationToolsDialog (QSettings * settings
|
|
, QDir const& data_directory
|
|
, QVector<double> const& coefficients
|
|
, QWidget * parent)
|
|
: m_ {this, settings, data_directory, coefficients, parent}
|
|
{
|
|
}
|
|
|
|
void EqualizationToolsDialog::show ()
|
|
{
|
|
m_->show ();
|
|
}
|
|
|
|
EqualizationToolsDialog::impl::impl (EqualizationToolsDialog * self
|
|
, QSettings * settings
|
|
, QDir const& data_directory
|
|
, QVector<double> const& coefficients
|
|
, QWidget * parent)
|
|
: QDialog {parent}
|
|
, self_ {self}
|
|
, settings_ {settings}
|
|
, data_directory_ {data_directory}
|
|
, current_coefficients_ {coefficients}
|
|
, amp_poly_low_ {0}
|
|
, amp_poly_high_ {6000}
|
|
, button_box_ {QDialogButtonBox::Apply
|
|
| QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Close
|
|
, Qt::Vertical}
|
|
{
|
|
setWindowTitle (windowTitle () + ' ' + tr (title));
|
|
resize (500, 600);
|
|
{
|
|
SettingsGroup g {settings_, title};
|
|
restoreGeometry (settings_->value ("geometry", saveGeometry ()).toByteArray ());
|
|
}
|
|
|
|
auto legend_title = new QCPTextElement {&plot_, tr ("Phase"), QFont {"sans", 9, QFont::Bold}};
|
|
legend_title->setLayer (plot_.legend->layer ());
|
|
plot_.legend->addElement (0, 0, legend_title);
|
|
plot_.legend->setVisible (true);
|
|
|
|
plot_.xAxis->setLabel (tr ("Freq (Hz)"));
|
|
plot_.xAxis->setRange (500, 2500);
|
|
plot_.yAxis->setLabel (tr ("Phase (Π)"));
|
|
plot_.yAxis->setRange (-1, +1);
|
|
plot_.yAxis2->setLabel (tr ("Delay (ms)"));
|
|
plot_.axisRect ()->setRangeDrag (Qt::Vertical);
|
|
plot_.axisRect ()->setRangeZoom (Qt::Vertical);
|
|
plot_.yAxis2->setVisible (true);
|
|
plot_.axisRect ()->setRangeDragAxes (nullptr, plot_.yAxis2);
|
|
plot_.axisRect ()->setRangeZoomAxes (nullptr, plot_.yAxis2);
|
|
plot_.axisRect ()->insetLayout ()->setInsetAlignment (0, Qt::AlignBottom|Qt::AlignRight);
|
|
plot_.setInteractions (QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
|
|
|
|
plot_.addGraph ()->setName (tr ("Measured"));
|
|
plot_.graph ()->setPen (QPen {Qt::blue});
|
|
plot_.graph ()->setVisible (false);
|
|
plot_.graph ()->removeFromLegend ();
|
|
|
|
plot_.addGraph ()->setName (tr ("Proposed"));
|
|
plot_.graph ()->setPen (QPen {Qt::red});
|
|
plot_.graph ()->setVisible (false);
|
|
plot_.graph ()->removeFromLegend ();
|
|
|
|
plot_.addGraph ()->setName (tr ("Current"));
|
|
plot_.graph ()->setPen (QPen {Qt::green});
|
|
|
|
plot_.addGraph (plot_.xAxis, plot_.yAxis2)->setName (tr ("Group Delay"));
|
|
plot_.graph ()->setPen (QPen {Qt::darkGreen});
|
|
|
|
plot_.plotLayout ()->addElement (new QCPAxisRect {&plot_});
|
|
plot_.plotLayout ()->setRowStretchFactor (1, 0.5);
|
|
|
|
auto amp_legend = new QCPLegend;
|
|
plot_.axisRect (1)->insetLayout ()->addElement (amp_legend, Qt::AlignTop | Qt::AlignRight);
|
|
plot_.axisRect (1)->insetLayout ()->setMargins (QMargins {12, 12, 12, 12});
|
|
amp_legend->setVisible (true);
|
|
amp_legend->setLayer (QLatin1String {"legend"});
|
|
legend_title = new QCPTextElement {&plot_, tr ("Amplitude"), QFont {"sans", 9, QFont::Bold}};
|
|
legend_title->setLayer (amp_legend->layer ());
|
|
amp_legend->addElement (0, 0, legend_title);
|
|
|
|
plot_.axisRect (1)->axis (QCPAxis::atBottom)->setLabel (tr ("Freq (Hz)"));
|
|
plot_.axisRect (1)->axis (QCPAxis::atBottom)->setRange (0, 6000);
|
|
plot_.axisRect (1)->axis (QCPAxis::atLeft)->setLabel (tr ("Relative Power (dB)"));
|
|
plot_.axisRect (1)->axis (QCPAxis::atLeft)->setRangeLower (0);
|
|
plot_.axisRect (1)->setRangeDragAxes (nullptr, nullptr);
|
|
plot_.axisRect (1)->setRangeZoomAxes (nullptr, nullptr);
|
|
|
|
plot_.addGraph (plot_.axisRect (1)->axis (QCPAxis::atBottom)
|
|
, plot_.axisRect (1)->axis (QCPAxis::atLeft))->setName (tr ("Reference"));
|
|
plot_.graph ()->setPen (QPen {Qt::blue});
|
|
plot_.graph ()->removeFromLegend ();
|
|
plot_.graph ()->addToLegend (amp_legend);
|
|
|
|
layout_.addWidget (&plot_);
|
|
|
|
auto load_phase_button = button_box_.addButton (tr ("Phase ..."), QDialogButtonBox::ActionRole);
|
|
auto refresh_button = button_box_.addButton (tr ("Refresh"), QDialogButtonBox::ActionRole);
|
|
auto discard_measured_button = button_box_.addButton (tr ("Discard Measured"), QDialogButtonBox::ActionRole);
|
|
layout_.addWidget (&button_box_);
|
|
setLayout (&layout_);
|
|
|
|
connect (&button_box_, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
|
connect (&button_box_, &QDialogButtonBox::clicked, [=] (QAbstractButton * button) {
|
|
if (button == load_phase_button)
|
|
{
|
|
plot_phase ();
|
|
}
|
|
else if (button == refresh_button)
|
|
{
|
|
plot_current ();
|
|
}
|
|
else if (button == button_box_.button (QDialogButtonBox::Apply))
|
|
{
|
|
if (plot_.graph (0)->dataCount ()) // something loaded
|
|
{
|
|
current_coefficients_ = new_coefficients_;
|
|
Q_EMIT self_->phase_equalization_changed (current_coefficients_);
|
|
plot_current ();
|
|
}
|
|
}
|
|
else if (button == button_box_.button (QDialogButtonBox::RestoreDefaults))
|
|
{
|
|
current_coefficients_ = QVector<double> {0., 0., 0., 0., 0.};
|
|
Q_EMIT self_->phase_equalization_changed (current_coefficients_);
|
|
plot_current ();
|
|
}
|
|
else if (button == discard_measured_button)
|
|
{
|
|
new_coefficients_ = QVector<double> {0., 0., 0., 0., 0.};
|
|
|
|
plot_.graph (0)->data ()->clear ();
|
|
plot_.graph (0)->setVisible (false);
|
|
plot_.graph (0)->removeFromLegend ();
|
|
|
|
plot_.graph (1)->data ()->clear ();
|
|
plot_.graph (1)->setVisible (false);
|
|
plot_.graph (1)->removeFromLegend ();
|
|
|
|
plot_.replot ();
|
|
}
|
|
});
|
|
|
|
plot_current ();
|
|
}
|
|
|
|
struct PowerSpectrumPoint
|
|
{
|
|
operator QCPGraphData () const
|
|
{
|
|
return QCPGraphData {freq_, power_};
|
|
}
|
|
|
|
float freq_;
|
|
float power_;
|
|
};
|
|
|
|
// read an amplitude point line from a stream (refspec.dat)
|
|
std::istream& operator >> (std::istream& is, PowerSpectrumPoint& r)
|
|
{
|
|
float y1, y3, y4; // discard these
|
|
is >> r.freq_ >> y1 >> r.power_ >> y3 >> y4;
|
|
return is;
|
|
}
|
|
|
|
void EqualizationToolsDialog::impl::plot_current ()
|
|
{
|
|
auto phase_graph = make_plot_data_loader (&plot_, 2, wrap_pi);
|
|
plot_.graph (2)->data ()->clear ();
|
|
std::generate_n (std::back_inserter (phase_graph), intervals + 1
|
|
, make_graph_generator (make_polynomial (current_coefficients_), freq_scaling, pi_scaling));
|
|
|
|
auto group_delay_graph = make_plot_data_loader (&plot_, 3, adjust_identity);
|
|
plot_.graph (3)->data ()->clear ();
|
|
std::generate_n (std::back_inserter (group_delay_graph), intervals + 1
|
|
, make_graph_generator (make_group_delay (current_coefficients_), freq_scaling, identity<double>));
|
|
plot_.graph (3)->rescaleValueAxis ();
|
|
|
|
QFileInfo refspec_file_info {data_directory_.absoluteFilePath ("refspec.dat")};
|
|
std::ifstream refspec_file (refspec_file_info.absoluteFilePath ().toLatin1 ().constData (), std::ifstream::in);
|
|
unsigned n;
|
|
if (refspec_file >> amp_poly_low_ >> amp_poly_high_ >> n)
|
|
{
|
|
std::istream_iterator<double> isi {refspec_file};
|
|
amp_coefficients_.clear ();
|
|
std::copy_n (isi, n, std::back_inserter (amp_coefficients_));
|
|
}
|
|
else
|
|
{
|
|
// may be old format refspec.dat with no header so rewind
|
|
refspec_file.clear ();
|
|
refspec_file.seekg (0);
|
|
}
|
|
|
|
auto reference_spectrum_graph = make_plot_data_loader (&plot_, 4, adjust_identity);
|
|
plot_.graph (4)->data ()->clear ();
|
|
std::copy (std::istream_iterator<PowerSpectrumPoint> {refspec_file},
|
|
std::istream_iterator<PowerSpectrumPoint> {},
|
|
std::back_inserter (reference_spectrum_graph));
|
|
plot_.graph (4)->rescaleValueAxis (true);
|
|
|
|
plot_.replot ();
|
|
}
|
|
|
|
struct PhasePoint
|
|
{
|
|
operator QCPGraphData () const
|
|
{
|
|
return QCPGraphData {freq_, phase_};
|
|
}
|
|
|
|
double freq_;
|
|
double phase_;
|
|
};
|
|
|
|
// read a phase point line from a stream (pcoeff file)
|
|
std::istream& operator >> (std::istream& is, PhasePoint& c)
|
|
{
|
|
double pp, sigmay; // discard these
|
|
if (is >> c.freq_ >> pp >> c.phase_ >> sigmay)
|
|
{
|
|
c.freq_ = 1500. + 1000. * c.freq_; // scale frequency to Hz
|
|
c.phase_ /= PI; // scale to units of Pi
|
|
}
|
|
return is;
|
|
}
|
|
|
|
void EqualizationToolsDialog::impl::plot_phase ()
|
|
{
|
|
auto const& phase_file_name = QFileDialog::getOpenFileName (this
|
|
, "Select Phase Response Coefficients"
|
|
, data_directory_.absolutePath ()
|
|
, "Phase Coefficient Files (*.pcoeff)");
|
|
if (!phase_file_name.size ()) return;
|
|
|
|
std::ifstream phase_file (phase_file_name.toLatin1 ().constData (), std::ifstream::in);
|
|
int n;
|
|
float chi;
|
|
float rmsdiff;
|
|
unsigned freq_low;
|
|
unsigned freq_high;
|
|
unsigned terms;
|
|
// read header information
|
|
if (phase_file >> n >> chi >> rmsdiff >> freq_low >> freq_high >> terms)
|
|
{
|
|
std::istream_iterator<double> isi {phase_file};
|
|
new_coefficients_.clear ();
|
|
std::copy_n (isi, terms, std::back_inserter (new_coefficients_));
|
|
|
|
if (phase_file)
|
|
{
|
|
plot_.graph (0)->data ()->clear ();
|
|
plot_.graph (1)->data ()->clear ();
|
|
|
|
// read the phase data and plot as graph 0
|
|
auto graph = make_plot_data_loader (&plot_, 0, adjust_identity);
|
|
std::copy_n (std::istream_iterator<PhasePoint> {phase_file},
|
|
intervals + 1, std::back_inserter (graph));
|
|
|
|
if (phase_file)
|
|
{
|
|
plot_.graph(0)->setLineStyle(QCPGraph::lsNone);
|
|
plot_.graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 4));
|
|
plot_.graph (0)->setVisible (true);
|
|
plot_.graph (0)->addToLegend ();
|
|
|
|
// generate the proposed polynomial plot as graph 1
|
|
auto graph = make_plot_data_loader (&plot_, 1, wrap_pi);
|
|
std::generate_n (std::back_inserter (graph), intervals + 1
|
|
, make_graph_generator (make_polynomial (new_coefficients_)
|
|
, freq_scaling, pi_scaling));
|
|
plot_.graph (1)->setVisible (true);
|
|
plot_.graph (1)->addToLegend ();
|
|
}
|
|
|
|
plot_.replot ();
|
|
}
|
|
}
|
|
}
|
|
|
|
#include "moc_EqualizationToolsDialog.cpp"
|