diff --git a/app/main.cpp b/app/main.cpp index 4d9d4d93b..bb5318197 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -50,6 +50,7 @@ #include "mainwindow.h" #include "remotetcpsinkstarter.h" #include "dsp/dsptypes.h" +#include "util/profiler.h" #include "crashhandler.h" static void logExceptionStackTrace() @@ -340,6 +341,10 @@ int main(int argc, char* argv[]) installCrashHandler(logger); #endif +#ifdef ENABLE_PROFILER + GlobalProfileData::resetProfileData(); // Start timer +#endif + int res = runQtApplication(argc, argv, logger); if (logger) { diff --git a/doc/img/MainWindow_spectrum_gui.png b/doc/img/MainWindow_spectrum_gui.png index 7530e9ebd..7b89d47b6 100644 Binary files a/doc/img/MainWindow_spectrum_gui.png and b/doc/img/MainWindow_spectrum_gui.png differ diff --git a/doc/img/MainWindow_spectrum_gui.xcf b/doc/img/MainWindow_spectrum_gui.xcf index 42951e886..0638418de 100644 Binary files a/doc/img/MainWindow_spectrum_gui.xcf and b/doc/img/MainWindow_spectrum_gui.xcf differ diff --git a/doc/img/MainWindow_spectrum_gui_D.xcf b/doc/img/MainWindow_spectrum_gui_D.xcf index da309ada5..f1b4c7bdb 100644 Binary files a/doc/img/MainWindow_spectrum_gui_D.xcf and b/doc/img/MainWindow_spectrum_gui_D.xcf differ diff --git a/doc/img/MainWindow_spectrum_gui_F.png b/doc/img/MainWindow_spectrum_gui_F.png index 37076537c..f3bd8e960 100644 Binary files a/doc/img/MainWindow_spectrum_gui_F.png and b/doc/img/MainWindow_spectrum_gui_F.png differ diff --git a/doc/img/MainWindow_spectrum_gui_F.xcf b/doc/img/MainWindow_spectrum_gui_F.xcf index ec72053c1..36dbc0413 100644 Binary files a/doc/img/MainWindow_spectrum_gui_F.xcf and b/doc/img/MainWindow_spectrum_gui_F.xcf differ diff --git a/doc/img/MainWindow_spectrum_gui_G.png b/doc/img/MainWindow_spectrum_gui_G.png new file mode 100644 index 000000000..83381ffd2 Binary files /dev/null and b/doc/img/MainWindow_spectrum_gui_G.png differ diff --git a/doc/img/MainWindow_spectrum_gui_G.xcf b/doc/img/MainWindow_spectrum_gui_G.xcf new file mode 100644 index 000000000..84038c087 Binary files /dev/null and b/doc/img/MainWindow_spectrum_gui_G.xcf differ diff --git a/doc/img/MainWindow_spectrum_gui_narrow.png b/doc/img/MainWindow_spectrum_gui_narrow.png index e461c4ca6..ad2dbe9d9 100644 Binary files a/doc/img/MainWindow_spectrum_gui_narrow.png and b/doc/img/MainWindow_spectrum_gui_narrow.png differ diff --git a/doc/img/MainWindow_spectrum_gui_narrow.xcf b/doc/img/MainWindow_spectrum_gui_narrow.xcf index 35360fc67..c2a680da2 100644 Binary files a/doc/img/MainWindow_spectrum_gui_narrow.xcf and b/doc/img/MainWindow_spectrum_gui_narrow.xcf differ diff --git a/doc/img/MainWindow_spectrum_gui_wide.png b/doc/img/MainWindow_spectrum_gui_wide.png index a569df96d..caf39bff5 100644 Binary files a/doc/img/MainWindow_spectrum_gui_wide.png and b/doc/img/MainWindow_spectrum_gui_wide.png differ diff --git a/doc/img/Spectrum_Display_Settings.png b/doc/img/Spectrum_Display_Settings.png new file mode 100644 index 000000000..8db22ccce Binary files /dev/null and b/doc/img/Spectrum_Display_Settings.png differ diff --git a/doc/img/Spectrum_Measurement_Mask_Test.png b/doc/img/Spectrum_Measurement_Mask_Test.png new file mode 100644 index 000000000..a5d213aef Binary files /dev/null and b/doc/img/Spectrum_Measurement_Mask_Test.png differ diff --git a/doc/img/Spectrum_Status.png b/doc/img/Spectrum_Status.png index 0dac7cf1d..de7abfba0 100644 Binary files a/doc/img/Spectrum_Status.png and b/doc/img/Spectrum_Status.png differ diff --git a/logging/bufferlogger.cpp b/logging/bufferlogger.cpp index 6b072f690..4f9b0bc6b 100644 --- a/logging/bufferlogger.cpp +++ b/logging/bufferlogger.cpp @@ -20,6 +20,7 @@ using namespace qtwebapp; BufferLogger::BufferLogger(int maxSize, QObject *parent) : + Logger(parent), m_maxSize(maxSize) { } @@ -41,7 +42,7 @@ QString BufferLogger::getLog() const { QString log; - for (const auto s : m_messages) { + for (const auto& s : m_messages) { log.append(s); } diff --git a/plugins/channelrx/demodils/ilsdemodgui.cpp b/plugins/channelrx/demodils/ilsdemodgui.cpp index 518fc91a6..51dcc30d2 100644 --- a/plugins/channelrx/demodils/ilsdemodgui.cpp +++ b/plugins/channelrx/demodils/ilsdemodgui.cpp @@ -1067,7 +1067,7 @@ ILSDemodGUI::ILSDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseban ui->glSpectrum->setCenterFrequency(0); ui->glSpectrum->setSampleRate(ILSDemodSettings::ILSDEMOD_SPECTRUM_SAMPLE_RATE); - ui->glSpectrum->setMeasurementParams(SpectrumSettings::MeasurementPeaks, 0, 1000, 90, 150, 1, 5, true, 1); + ui->glSpectrum->setMeasurementParams(SpectrumSettings::MeasurementPeaks, 0, 1000, 90, 150, 1, 5, true, 1, 0); ui->glSpectrum->setMeasurementsVisible(true); m_channelMarker.setColor(Qt::yellow); diff --git a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp index dbdf06e18..57cdaab47 100644 --- a/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp +++ b/plugins/channelrx/demodmeshtastic/meshtasticdemodgui.cpp @@ -2596,7 +2596,7 @@ void MeshtasticDemodGUI::replayDechirpSnapshot(const DechirpSnapshot& snapshot) } } - ui->glSpectrum->newSpectrum(line.data(), fftSize, fftSize); + ui->glSpectrum->newSpectrum(line.data(), fftSize); }; // Prime the GL spectrum so pending size/layout changes are applied before replay. diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 244e63bee..923629827 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -487,6 +487,7 @@ set(sdrbase_HEADERS util/azel.h util/baudot.h util/callsign.h + util/circularbuffer.h util/colormap.h util/coordinates.h util/corsproxy.h diff --git a/sdrbase/channel/channelwebapiutils.cpp b/sdrbase/channel/channelwebapiutils.cpp index b6754f555..6731f00b7 100644 --- a/sdrbase/channel/channelwebapiutils.cpp +++ b/sdrbase/channel/channelwebapiutils.cpp @@ -2167,6 +2167,8 @@ void DeviceOpener::deviceSetAdded(int index, DeviceAPI *device) void DeviceOpener::deviceChanged(int index) { + (void) index; + // Apply device settings QString errorMessage; if (200 != m_device->getSampleSource()->webapiSettingsPutPatch(false, m_settingsKeys, *m_response, errorMessage)) { diff --git a/sdrbase/dsp/dspengine.cpp b/sdrbase/dsp/dspengine.cpp index da86975a8..1ef605d51 100644 --- a/sdrbase/dsp/dspengine.cpp +++ b/sdrbase/dsp/dspengine.cpp @@ -65,6 +65,7 @@ DSPDeviceSourceEngine *DSPEngine::addDeviceSourceEngine() { auto *deviceSourceEngine = new DSPDeviceSourceEngine(m_deviceSourceEnginesUIDSequence); auto *deviceThread = new QThread(); + deviceThread->setObjectName(QString("DSPDeviceSourceEngine thread %1").arg(m_deviceSourceEnginesUIDSequence)); m_deviceSourceEnginesUIDSequence++; m_deviceSourceEngines.push_back(deviceSourceEngine); m_deviceEngineReferences.push_back(DeviceEngineReference{0, m_deviceSourceEngines.back(), nullptr, nullptr, deviceThread}); @@ -107,6 +108,7 @@ DSPDeviceSinkEngine *DSPEngine::addDeviceSinkEngine() { auto *deviceSinkEngine = new DSPDeviceSinkEngine(m_deviceSinkEnginesUIDSequence); auto *deviceThread = new QThread(); + deviceThread->setObjectName(QString("DSPDeviceSinkEngine thread %1").arg(m_deviceSinkEnginesUIDSequence)); m_deviceSinkEnginesUIDSequence++; m_deviceSinkEngines.push_back(deviceSinkEngine); m_deviceEngineReferences.push_back(DeviceEngineReference{1, nullptr, m_deviceSinkEngines.back(), nullptr, deviceThread}); @@ -149,6 +151,7 @@ DSPDeviceMIMOEngine *DSPEngine::addDeviceMIMOEngine() { auto *deviceMIMOEngine = new DSPDeviceMIMOEngine(m_deviceMIMOEnginesUIDSequence); auto *deviceThread = new QThread(); + deviceThread->setObjectName(QString("DSPDeviceMIMOEngine thread %1").arg(m_deviceMIMOEnginesUIDSequence)); m_deviceMIMOEnginesUIDSequence++; m_deviceMIMOEngines.push_back(deviceMIMOEngine); m_deviceEngineReferences.push_back(DeviceEngineReference{2, nullptr, nullptr, m_deviceMIMOEngines.back(), deviceThread}); diff --git a/sdrbase/dsp/glspectruminterface.h b/sdrbase/dsp/glspectruminterface.h index 1644ef9de..72581e543 100644 --- a/sdrbase/dsp/glspectruminterface.h +++ b/sdrbase/dsp/glspectruminterface.h @@ -28,10 +28,9 @@ class GLSpectrumInterface public: GLSpectrumInterface() {} virtual ~GLSpectrumInterface() {} - virtual void newSpectrum(const Real* spectrum, int nbBins, int fftSize) + virtual void newSpectrum(const Real* spectrum, int fftSize) { (void) spectrum; - (void) nbBins; (void) fftSize; } }; diff --git a/sdrbase/dsp/nco.cpp b/sdrbase/dsp/nco.cpp index 68c1e3e9f..d148b7f17 100644 --- a/sdrbase/dsp/nco.cpp +++ b/sdrbase/dsp/nco.cpp @@ -27,11 +27,13 @@ bool NCO::m_tableInitialized = false; void NCO::initTable() { - if(m_tableInitialized) + if (m_tableInitialized) { return; + } - for(int i = 0; i < TableSize; i++) + for (unsigned i = 0; i < TableSize; i++) { m_table[i] = cos((2.0 * M_PI * i) / TableSize); + } m_tableInitialized = true; } diff --git a/sdrbase/dsp/spectrumsettings.cpp b/sdrbase/dsp/spectrumsettings.cpp index 33f1d2138..2f51e5872 100644 --- a/sdrbase/dsp/spectrumsettings.cpp +++ b/sdrbase/dsp/spectrumsettings.cpp @@ -45,7 +45,7 @@ void SpectrumSettings::resetToDefaults() m_decay = 1; m_decayDivisor = 1; m_histogramStroke = 30; - m_displayGridIntensity = 5; + m_displayGridIntensity = 15; m_displayTraceIntensity = 50; m_waterfallShare = 0.5; m_displayCurrent = true; @@ -83,12 +83,46 @@ void SpectrumSettings::resetToDefaults() m_measurementPrecision = 1; m_findHistogramPeaks = false; #ifdef ANDROID - m_showAllControls = false; + m_showControls = ShowMinimum; #else - m_showAllControls = true; + m_showControls = ShowStandard; #endif m_frequencyZoomFactor = 1.0f; m_frequencyZoomPos = 0.5f; + m_waterfallTimeUnits = TimeOffset; + m_waterfallTimeFormat = "hh:mm:ss"; + m_scrollBar = false; + m_scrollLength = 100000; + m_mathMode = MathModeNone; + m_mathAvgCount = 100; + m_measurementMemMasks = 0x1; + m_displayRBW = false; + m_displayCursorStats = false; + m_displayPeakStats = false; + + m_spectrumMemory.clear(); + while (m_spectrumMemory.size() < m_maxSpectrumMemories) + { + SpectrumMemory memory; + memory.m_display = false; + memory.m_color = QColor(Qt::cyan).darker().rgb(); + memory.m_label = ""; + m_spectrumMemory.append(memory); + } + + m_spectrumColor = qRgb(255, 255, 63); +} + +// Ensure we have settings for each spectrum memory +void SpectrumSettings::validateSpectrumMemories() +{ + while (m_spectrumMemory.size() < m_maxSpectrumMemories) + { + SpectrumMemory memory; + memory.m_display = false; + memory.m_color = QColor(Qt::cyan).darker().rgb(); + m_spectrumMemory.append(memory); + } } QByteArray SpectrumSettings::serialize() const @@ -141,9 +175,23 @@ QByteArray SpectrumSettings::serialize() const s.writeS32(46, m_measurementCenterFrequencyOffset); s.writeBool(47, m_findHistogramPeaks); s.writeBool(48, m_truncateFreqScale); - s.writeBool(49, m_showAllControls); + // 49 was showAllControls - replaced by m_showControls s.writeFloat(50, m_frequencyZoomFactor); s.writeFloat(51, m_frequencyZoomPos); + s.writeS32(52, (int) m_waterfallTimeUnits); + s.writeString(53, m_waterfallTimeFormat); + s.writeBool(54, m_scrollBar); + s.writeS32(55, m_scrollLength); + s.writeS32(56, (int) m_mathMode); + s.writeU32(57, m_mathAvgCount); + s.writeU32(58, m_measurementMemMasks); + s.writeBool(59, m_displayRBW); + s.writeBool(60, m_displayCursorStats); + s.writeBool(61, m_displayPeakStats); + s.writeList(62, m_spectrumMemory); + s.writeS32(63, (int) m_showControls); + s.writeU32(64, m_spectrumColor); + s.writeS32(100, m_histogramMarkers.size()); for (int i = 0; i < m_histogramMarkers.size(); i++) { @@ -207,14 +255,14 @@ bool SpectrumSettings::deserialize(const QByteArray& data) d.readBool(9, &m_displayHistogram, false); d.readS32(10, &m_decay, 1); d.readBool(11, &m_displayGrid, false); - d.readS32(13, &m_displayGridIntensity, 5); + d.readS32(13, &m_displayGridIntensity, 15); d.readS32(14, &m_decayDivisor, 1); d.readS32(15, &m_histogramStroke, 30); d.readBool(16, &m_displayCurrent, true); d.readS32(17, &m_displayTraceIntensity, 50); d.readReal(18, &m_waterfallShare, 0.66); d.readS32(19, &tmp, 0); - m_averagingMode = tmp < 0 ? AvgModeNone : tmp > 3 ? AvgModeMax : (AveragingMode) tmp; + m_averagingMode = tmp < 0 ? AvgModeNone : tmp > 4 ? AvgModeMin : (AveragingMode) tmp; d.readS32(20, &tmp, 0); m_averagingIndex = getAveragingIndex(tmp, m_averagingMode); m_averagingValue = getAveragingValue(m_averagingIndex, m_averagingMode); @@ -248,13 +296,26 @@ bool SpectrumSettings::deserialize(const QByteArray& data) d.readS32(46, &m_measurementCenterFrequencyOffset, 0); d.readBool(47, &m_findHistogramPeaks, false); d.readBool(48, &m_truncateFreqScale, false); -#ifdef ANDROID - d.readBool(49, &m_showAllControls, false); -#else - d.readBool(49, &m_showAllControls, true); -#endif d.readFloat(50, &m_frequencyZoomFactor, 1.0f); d.readFloat(51, &m_frequencyZoomPos, 0.5f); + d.readS32(52, (int *) &m_waterfallTimeUnits, TimeOffset); + d.readString(53, &m_waterfallTimeFormat, "hh:mm:ss"); + d.readBool(54, &m_scrollBar, false); + d.readS32(55, &m_scrollLength, 100000); + d.readS32(56, (int *) &m_mathMode, (int) MathModeNone); + d.readU32(57, &m_mathAvgCount, 100); + d.readU32(58, &m_measurementMemMasks, 0x1); + d.readBool(59, &m_displayRBW, false); + d.readBool(60, &m_displayCursorStats, false); + d.readBool(61, &m_displayPeakStats, false); + d.readList(62, &m_spectrumMemory); + validateSpectrumMemories(); +#ifdef ANDROID + d.readS32(63, (int *) &m_showControls, ShowMinimum); +#else + d.readS32(63, (int *) &m_showControls, ShowStandard); +#endif + d.readU32(64, &m_spectrumColor, qRgb(255, 255, 63)); int histogramMarkersSize; d.readS32(100, &histogramMarkersSize, 0); @@ -585,7 +646,7 @@ void SpectrumSettings::updateFrom(const QStringList& keys, const SWGSDRangel::SW int SpectrumSettings::getAveragingMaxScale(AveragingMode averagingMode) { - if (averagingMode == AvgModeMoving) { + if (averagingMode == AvgModeMoving) { return 3; // max 10k } else { return 5; // max 1M @@ -671,3 +732,73 @@ QColor SpectrumSettings::intToQColor(int intColor) int b = bg / 256; return QColor(r, g, b); } + +bool SpectrumSettings::mathAverageUsed() const +{ + return (m_mathMode == SpectrumSettings::MathModeXMinusAvg) + || (m_mathMode == SpectrumSettings::MathModeXMinusAvgDB) + || (m_mathMode == SpectrumSettings::MathModeXMinusAvgPlusMinAvgDB) + || (m_mathMode == SpectrumSettings::MathModeAbsXMinusAvgDB); +} + +bool SpectrumSettings::mathUsesDB() const +{ + return (m_mathMode == SpectrumSettings::MathModeXMinusAvgDB) + || (m_mathMode == SpectrumSettings::MathModeXMinusAvgPlusMinAvgDB) + || (m_mathMode == SpectrumSettings::MathModeAbsXMinusAvgDB) + || (m_mathMode == SpectrumSettings::MathModeXMinusM1DB) + || (m_mathMode == SpectrumSettings::MathModeAbsXMinusM1DB) + || (m_mathMode == SpectrumSettings::MathModeXMinusM2DB) + || (m_mathMode == SpectrumSettings::MathModeAbsXMinusM2DB) + ; +} + +QByteArray SpectrumSettings::SpectrumMemory::serialize() const +{ + SimpleSerializer s(1); + + s.writeList(1, m_spectrum); + s.writeBool(2, m_display); + s.writeU32(3, m_color); + s.writeString(4, m_label); + + return s.final(); +} + +bool SpectrumSettings::SpectrumMemory::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) { + return false; + } + + if (d.getVersion() == 1) + { + d.readList(1, &m_spectrum); + d.readBool(2, &m_display, false); + d.readU32(3, &m_color, QColor(Qt::cyan).darker().rgb()); + d.readString(4, &m_label); + + return true; + } + else + { + return false; + } + +} + +QDataStream& operator<<(QDataStream& out, const SpectrumSettings::SpectrumMemory& settings) +{ + out << settings.serialize(); + return out; +} + +QDataStream& operator>>(QDataStream& in, SpectrumSettings::SpectrumMemory& settings) +{ + QByteArray data; + in >> data; + settings.deserialize(data); + return in; +} diff --git a/sdrbase/dsp/spectrumsettings.h b/sdrbase/dsp/spectrumsettings.h index 8432e3c04..127ec19b4 100644 --- a/sdrbase/dsp/spectrumsettings.h +++ b/sdrbase/dsp/spectrumsettings.h @@ -2,7 +2,7 @@ // Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // // written by Christian Daniel // // Copyright (C) 2015-2022 Edouard Griffiths, F4EXB // -// Copyright (C) 2022 Jon Beniston, M7RCE // +// Copyright (C) 2022-2026 Jon Beniston, M7RCE // // // // 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 // @@ -34,12 +34,29 @@ class SDRBASE_API SpectrumSettings : public Serializable { public: + enum AveragingMode { AvgModeNone, AvgModeMoving, AvgModeFixed, - AvgModeMax + AvgModeMax, + AvgModeMin + }; + + enum MathMode + { + MathModeNone, + MathModeXMinusAvg, + MathModeXMinusAvgDB, + MathModeXMinusAvgPlusMinAvgDB, + MathModeAbsXMinusAvgDB, + MathModeXMinusM1, + MathModeXMinusM1DB, + MathModeAbsXMinusM1DB, + MathModeXMinusM2, + MathModeXMinusM2DB, + MathModeAbsXMinusM2DB }; // Bitmask for which selection of markers to display @@ -81,7 +98,8 @@ public: MeasurementAdjacentChannelPower, MeasurementOccupiedBandwidth, Measurement3dBBandwidth, - MeasurementSNR + MeasurementSNR, + MeasurementMask }; enum MeasurementsPosition { @@ -91,6 +109,28 @@ public: PositionRight }; + enum WaterfallTimeUnits { + TimeOffset, + LocalTime, + UTCTime + }; + + enum ShowControls { + ShowMinimum, + ShowStandard, + ShowAll + }; + + struct SpectrumMemory { + QList m_spectrum; + bool m_display; + QRgb m_color; + QString m_label; + + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + }; + int m_fftSize; int m_fftOverlap; FFTWindow::Function m_fftWindow; @@ -141,9 +181,23 @@ public: bool m_measurementHighlight; MeasurementsPosition m_measurementsPosition; int m_measurementPrecision; - bool m_showAllControls; + enum ShowControls m_showControls; float m_frequencyZoomFactor; float m_frequencyZoomPos; + WaterfallTimeUnits m_waterfallTimeUnits; + QString m_waterfallTimeFormat; //!< E.g: "hh:mm:ss.zzz" + bool m_scrollBar; + int m_scrollLength; + MathMode m_mathMode; + unsigned int m_mathAvgCount; + unsigned int m_measurementMemMasks; // Bitmask for each memory to be used as a mask + bool m_displayRBW; + bool m_displayCursorStats; + bool m_displayPeakStats; + QList m_spectrumMemory; + QRgb m_spectrumColor; + + static const int m_maxSpectrumMemories = 2; // As we only have two buttons in GUI static const int m_log2FFTSizeMin = 6; // 64 static const int m_log2FFTSizeMax = 15; // 32k @@ -151,6 +205,7 @@ public: SpectrumSettings(); virtual ~SpectrumSettings(); void resetToDefaults(); + void validateSpectrumMemories(); virtual QByteArray serialize() const; virtual bool deserialize(const QByteArray& data); @@ -160,6 +215,8 @@ public: QList& getHistogramMarkers() { return m_histogramMarkers; } QList& getWaterfallMarkers() { return m_waterfallMarkers; } bool getHistogramFindPeaks() { return m_findHistogramPeaks; } + bool mathAverageUsed() const; + bool mathUsesDB() const; static int getAveragingMaxScale(AveragingMode averagingMode); //!< Max power of 10 multiplier to 2,5,10 base ex: 2 -> 2,5,10,20,50,100,200,500,1000 static int getAveragingValue(int averagingIndex, AveragingMode averagingMode); diff --git a/sdrbase/dsp/spectrumvis.cpp b/sdrbase/dsp/spectrumvis.cpp index eaf8b005c..f0e0ef93e 100644 --- a/sdrbase/dsp/spectrumvis.cpp +++ b/sdrbase/dsp/spectrumvis.cpp @@ -6,6 +6,7 @@ // Copyright (C) 2022 Jiří Pinkava // // Copyright (C) 2023 Arne Jünemann // // Copyright (C) 2023 Vladimir Pleskonjic // +// Copyright (C) 2026 Jon Beniston, M7RCE // // // // 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 // @@ -30,6 +31,7 @@ #include "dspengine.h" #include "fftfactory.h" #include "util/messagequeue.h" +#include "util/profiler.h" #include "spectrumvis.h" @@ -38,7 +40,6 @@ MESSAGE_CLASS_DEFINITION(SpectrumVis::MsgConfigureScalingFactor, Message) MESSAGE_CLASS_DEFINITION(SpectrumVis::MsgConfigureWSpectrumOpenClose, Message) MESSAGE_CLASS_DEFINITION(SpectrumVis::MsgConfigureWSpectrum, Message) MESSAGE_CLASS_DEFINITION(SpectrumVis::MsgStartStop, Message) -MESSAGE_CLASS_DEFINITION(SpectrumVis::MsgFrequencyZooming, Message) const Real SpectrumVis::m_mult = (10.0f / log2(10.0f)); @@ -49,7 +50,7 @@ SpectrumVis::SpectrumVis(Real scalef) : m_fftEngineSequence(0), m_fftBuffer(4096), m_powerSpectrum(4096), - m_psd(4096), + m_mathMemory(4096), m_fftBufferFill(0), m_needMoreSamples(false), m_scalef(scalef), @@ -57,8 +58,7 @@ SpectrumVis::SpectrumVis(Real scalef) : m_specMax(0.0f), m_centerFrequency(0), m_sampleRate(48000), - m_ofs(0), - m_powFFTDiv(1.0), + m_powFFTMul(1.0f), m_guiMessageQueue(nullptr) { setObjectName("SpectrumVis"); @@ -107,206 +107,15 @@ void SpectrumVis::feedTriggered(const SampleVector::const_iterator& triggerPoint void SpectrumVis::feed(const Complex *begin, unsigned int length) { - if (!m_glSpectrum && !m_wsSpectrum.socketOpened()) { - return; - } + if (!m_glSpectrum && !m_wsSpectrum.socketOpened()) { + return; + } if (!m_mutex.tryLock(0)) { // prevent conflicts with configuration process return; } - Complex c; - Real v; - int fftMin = (m_settings.m_frequencyZoomFactor == 1.0f) ? - 0 : (m_settings.m_frequencyZoomPos - (0.5f / m_settings.m_frequencyZoomFactor)) * m_settings.m_fftSize; - int fftMax = (m_settings.m_frequencyZoomFactor == 1.0f) ? - m_settings.m_fftSize : (m_settings.m_frequencyZoomPos + (0.5f / m_settings.m_frequencyZoomFactor)) * m_settings.m_fftSize; - - if (m_settings.m_averagingMode == SpectrumSettings::AvgModeNone) - { - for (int i = 0; i < m_settings.m_fftSize; i++) - { - if (i < (int) length) { - c = begin[i]; - } else { - c = Complex{0,0}; - } - - v = c.real() * c.real() + c.imag() * c.imag(); - m_psd[i] = v/m_powFFTDiv; - v = m_settings.m_linear ? v/m_powFFTDiv : m_mult * log2fapprox(v) + m_ofs; - m_powerSpectrum[i] = v; - } - - // send new data to visualisation - if (m_glSpectrum) - { - m_glSpectrum->newSpectrum( - &m_powerSpectrum.data()[fftMin], - fftMax - fftMin, - m_settings.m_fftSize - ); - } - - // web socket spectrum connections - if (m_wsSpectrum.socketOpened()) - { - m_wsSpectrum.newSpectrum( - m_powerSpectrum, - m_settings.m_fftSize, - m_centerFrequency, - m_sampleRate, - m_settings.m_linear, - m_settings.m_ssb, - m_settings.m_usb - ); - } - } - else if (m_settings.m_averagingMode == SpectrumSettings::AvgModeMoving) - { - for (int i = 0; i < m_settings.m_fftSize; i++) - { - if (i < (int) length) { - c = begin[i]; - } else { - c = Complex{0,0}; - } - - v = c.real() * c.real() + c.imag() * c.imag(); - v = m_movingAverage.storeAndGetAvg(v, i); - m_psd[i] = v/m_powFFTDiv; - v = m_settings.m_linear ? v/m_powFFTDiv : m_mult * log2fapprox(v) + m_ofs; - m_powerSpectrum[i] = v; - } - - // send new data to visualisation - if (m_glSpectrum) - { - m_glSpectrum->newSpectrum( - &m_powerSpectrum.data()[fftMin], - fftMax - fftMin, - m_settings.m_fftSize - ); - } - - // web socket spectrum connections - if (m_wsSpectrum.socketOpened()) - { - m_wsSpectrum.newSpectrum( - m_powerSpectrum, - m_settings.m_fftSize, - m_centerFrequency, - m_sampleRate, - m_settings.m_linear, - m_settings.m_ssb, - m_settings.m_usb - ); - } - - m_movingAverage.nextAverage(); - } - else if (m_settings.m_averagingMode == SpectrumSettings::AvgModeFixed) - { - double avg; - - for (int i = 0; i < m_settings.m_fftSize; i++) - { - if (i < (int) length) { - c = begin[i]; - } else { - c = Complex{0,0}; - } - - v = c.real() * c.real() + c.imag() * c.imag(); - - // result available - if (m_fixedAverage.storeAndGetAvg(avg, v, i)) - { - m_psd[i] = avg/m_powFFTDiv; - avg = m_settings.m_linear ? avg/m_powFFTDiv : m_mult * log2fapprox(avg) + m_ofs; - m_powerSpectrum[i] = avg; - } - } - - // result available - if (m_fixedAverage.nextAverage()) - { - // send new data to visualisation - if (m_glSpectrum) - { - m_glSpectrum->newSpectrum( - &m_powerSpectrum.data()[fftMin], - fftMax - fftMin, - m_settings.m_fftSize - ); - } - - // web socket spectrum connections - if (m_wsSpectrum.socketOpened()) - { - m_wsSpectrum.newSpectrum( - m_powerSpectrum, - m_settings.m_fftSize, - m_centerFrequency, - m_sampleRate, - m_settings.m_linear, - m_settings.m_ssb, - m_settings.m_usb - ); - } - } - } - else if (m_settings.m_averagingMode == SpectrumSettings::AvgModeMax) - { - double max; - - for (int i = 0; i < m_settings.m_fftSize; i++) - { - if (i < (int) length) { - c = begin[i]; - } else { - c = Complex{0,0}; - } - - v = c.real() * c.real() + c.imag() * c.imag(); - - // result available - if (m_max.storeAndGetMax(max, v, i)) - { - m_psd[i] = max/m_powFFTDiv; - max = m_settings.m_linear ? max/m_powFFTDiv : m_mult * log2fapprox(max) + m_ofs; - m_powerSpectrum[i] = max; - } - } - - // result available - if (m_max.nextMax()) - { - // send new data to visualisation - if (m_glSpectrum) - { - m_glSpectrum->newSpectrum( - &m_powerSpectrum.data()[fftMin], - fftMax - fftMin, - m_settings.m_fftSize - ); - } - - // web socket spectrum connections - if (m_wsSpectrum.socketOpened()) - { - m_wsSpectrum.newSpectrum( - m_powerSpectrum, - m_settings.m_fftSize, - m_centerFrequency, - m_sampleRate, - m_settings.m_linear, - m_settings.m_ssb, - m_settings.m_usb - ); - } - } - } + processFFT(begin, false, false, length); m_mutex.unlock(); } @@ -339,7 +148,7 @@ void SpectrumVis::feed(const ComplexVector::const_iterator& cbegin, const Comple std::copy(begin, begin + samplesNeeded, m_fftBuffer.begin() + m_fftBufferFill); begin += samplesNeeded; - processFFT(positiveOnly); + performFFT(positiveOnly); // advance buffer respecting the fft overlap factor // undefined behavior if the memory regions overlap, valid code for 50% overlap @@ -393,7 +202,7 @@ void SpectrumVis::feed(const SampleVector::const_iterator& cbegin, const SampleV *it++ = Complex(begin->real() / m_scalef, begin->imag() / m_scalef); } - processFFT(positiveOnly); + performFFT(positiveOnly); // advance buffer respecting the fft overlap factor // undefined behavior if the memory regions overlap, valid code for 50% overlap @@ -418,13 +227,61 @@ void SpectrumVis::feed(const SampleVector::const_iterator& cbegin, const SampleV m_mutex.unlock(); } -void SpectrumVis::processFFT(bool positiveOnly) +// Compute math operation on linear spectrum values +void SpectrumVis::mathLinear(std::vector &spectrum) { - int fftMin = (m_settings.m_frequencyZoomFactor == 1.0f) ? - 0 : (m_settings.m_frequencyZoomPos - (0.5f / m_settings.m_frequencyZoomFactor)) * m_settings.m_fftSize; - int fftMax = (m_settings.m_frequencyZoomFactor == 1.0f) ? - m_settings.m_fftSize : (m_settings.m_frequencyZoomPos + (0.5f / m_settings.m_frequencyZoomFactor)) * m_settings.m_fftSize; + if (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusAvg) + { + for (std::size_t i = 0; i < spectrum.size(); i++) { + spectrum[i] = std::max(spectrum[i] - (Real) m_mathMovingAverage.storeAndGetAvg(spectrum[i], i), 0.0f); + } + } + else if ((m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM1) || (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM2)) + { + for (std::size_t i = 0; i < spectrum.size(); i++) { + spectrum[i] = std::max(spectrum[i] - m_mathMemory[i], 0.0f); + } + } +} +// Compute math operation on dB spectrum values +void SpectrumVis::mathDB(std::vector &spectrum) +{ + if (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusAvgDB) + { + for (std::size_t i = 0; i < spectrum.size(); i++) { + spectrum[i] -= m_mathMovingAverage.storeAndGetAvg(spectrum[i], i); + } + } + else if (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusAvgPlusMinAvgDB) + { + Real minAvg = m_mathMovingAverage.getMin(); + for (std::size_t i = 0; i < spectrum.size(); i++) { + spectrum[i] = spectrum[i] - m_mathMovingAverage.storeAndGetAvg(spectrum[i], i) + minAvg; + } + } + else if (m_settings.m_mathMode == SpectrumSettings::MathModeAbsXMinusAvgDB) + { + for (std::size_t i = 0; i < spectrum.size(); i++) { + spectrum[i] = abs(spectrum[i] - m_mathMovingAverage.storeAndGetAvg(spectrum[i], i)); + } + } + else if ((m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM1DB) || (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM2DB)) + { + for (std::size_t i = 0; i < spectrum.size(); i++) { + spectrum[i] -= m_mathMemory[i]; + } + } + else if ((m_settings.m_mathMode == SpectrumSettings::MathModeAbsXMinusM1DB) || (m_settings.m_mathMode == SpectrumSettings::MathModeAbsXMinusM2DB)) + { + for (std::size_t i = 0; i < spectrum.size(); i++) { + spectrum[i] = abs(spectrum[i] - m_mathMemory[i]); + } + } +} + +void SpectrumVis::performFFT(bool positiveOnly) +{ // apply fft window (and copy from m_fftBuffer to m_fftIn) m_window.apply(&m_fftBuffer[0], m_fft->in()); @@ -432,313 +289,357 @@ void SpectrumVis::processFFT(bool positiveOnly) m_fft->transform(); // extract power spectrum and reorder buckets - const Complex* fftOut = m_fft->out(); + processFFT(m_fft->out(), true, positiveOnly, m_settings.m_fftSize); +} + +void SpectrumVis::processFFT(const Complex* fftOut, bool reorder, bool positiveOnly, int fftSize) +{ + PROFILER_START(); + Complex c; Real v; - std::size_t halfSize = m_settings.m_fftSize / 2; + int halfSize = fftSize / 2; + bool ready = false; if (m_settings.m_averagingMode == SpectrumSettings::AvgModeNone) { - m_specMax = 0.0f; - - if ( positiveOnly ) + if (positiveOnly) { - for (std::size_t i = 0; i < halfSize; i++) + for (int i = 0; i < halfSize; i++) { c = fftOut[i]; v = c.real() * c.real() + c.imag() * c.imag(); - m_psd[i] = v/m_powFFTDiv; - m_specMax = v > m_specMax ? v : m_specMax; - v = m_settings.m_linear ? v/m_powFFTDiv : m_mult * log2fapprox(v) + m_ofs; + v *= m_powFFTMul; m_powerSpectrum[i * 2] = v; m_powerSpectrum[i * 2 + 1] = v; } } - else + else if (reorder) { - for (std::size_t i = 0; i < halfSize; i++) + for (int i = 0; i < halfSize; i++) { c = fftOut[i + halfSize]; v = c.real() * c.real() + c.imag() * c.imag(); - m_psd[i] = v/m_powFFTDiv; - m_specMax = v > m_specMax ? v : m_specMax; - v = m_settings.m_linear ? v/m_powFFTDiv : m_mult * log2fapprox(v) + m_ofs; + v *= m_powFFTMul; m_powerSpectrum[i] = v; c = fftOut[i]; v = c.real() * c.real() + c.imag() * c.imag(); - m_psd[i + halfSize] = v/m_powFFTDiv; - m_specMax = v > m_specMax ? v : m_specMax; - v = m_settings.m_linear ? v/m_powFFTDiv : m_mult * log2fapprox(v) + m_ofs; + v *= m_powFFTMul; m_powerSpectrum[i + halfSize] = v; } } - - // send new data to visualisation - if (m_glSpectrum) + else { - m_glSpectrum->newSpectrum( - &m_powerSpectrum.data()[fftMin], - fftMax - fftMin, - m_settings.m_fftSize - ); + for (int i = 0; i < fftSize; i++) + { + c = fftOut[i]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + m_powerSpectrum[i] = v; + } } - // web socket spectrum connections - if (m_wsSpectrum.socketOpened()) - { - m_wsSpectrum.newSpectrum( - m_powerSpectrum, - m_settings.m_fftSize, - m_centerFrequency, - m_sampleRate, - m_settings.m_linear, - m_settings.m_ssb, - m_settings.m_usb - ); - } + ready = true; } else if (m_settings.m_averagingMode == SpectrumSettings::AvgModeMoving) { - m_specMax = 0.0f; + double avg; - if ( positiveOnly ) + if (positiveOnly) { - for (std::size_t i = 0; i < halfSize; i++) + for (int i = 0; i < halfSize; i++) { c = fftOut[i]; v = c.real() * c.real() + c.imag() * c.imag(); - v = m_movingAverage.storeAndGetAvg(v, i); - m_psd[i] = v/m_powFFTDiv; - m_specMax = v > m_specMax ? v : m_specMax; - v = m_settings.m_linear ? v/m_powFFTDiv : m_mult * log2fapprox(v) + m_ofs; - m_powerSpectrum[i * 2] = v; - m_powerSpectrum[i * 2 + 1] = v; + v *= m_powFFTMul; + avg = m_movingAverage.storeAndGetAvg(v, i); + m_powerSpectrum[i * 2] = (Real) avg; + m_powerSpectrum[i * 2 + 1] = (Real) avg; + } + } + else if (reorder) + { + for (int i = 0; i < halfSize; i++) + { + c = fftOut[i + halfSize]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + avg = m_movingAverage.storeAndGetAvg(v, i+halfSize); + m_powerSpectrum[i] = (Real) avg; + + c = fftOut[i]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + avg = m_movingAverage.storeAndGetAvg(v, i); + m_powerSpectrum[i + halfSize] = (Real) avg; } } else { - for (std::size_t i = 0; i < halfSize; i++) + for (int i = 0; i < fftSize; i++) { - c = fftOut[i + halfSize]; - v = c.real() * c.real() + c.imag() * c.imag(); - v = m_movingAverage.storeAndGetAvg(v, i+halfSize); - m_psd[i] = v/m_powFFTDiv; - m_specMax = v > m_specMax ? v : m_specMax; - v = m_settings.m_linear ? v/m_powFFTDiv : m_mult * log2fapprox(v) + m_ofs; - m_powerSpectrum[i] = v; - c = fftOut[i]; v = c.real() * c.real() + c.imag() * c.imag(); - v = m_movingAverage.storeAndGetAvg(v, i); - m_psd[i + halfSize] = v/m_powFFTDiv; - m_specMax = v > m_specMax ? v : m_specMax; - v = m_settings.m_linear ? v/m_powFFTDiv : m_mult * log2fapprox(v) + m_ofs; - m_powerSpectrum[i + halfSize] = v; + v *= m_powFFTMul; + avg = m_movingAverage.storeAndGetAvg(v, i); + m_powerSpectrum[i] = (Real) avg; } } - // send new data to visualisation - if (m_glSpectrum) - { - m_glSpectrum->newSpectrum( - &m_powerSpectrum.data()[fftMin], - fftMax - fftMin, - m_settings.m_fftSize - ); - } - - // web socket spectrum connections - if (m_wsSpectrum.socketOpened()) - { - m_wsSpectrum.newSpectrum( - m_powerSpectrum, - m_settings.m_fftSize, - m_centerFrequency, - m_sampleRate, - m_settings.m_linear, - m_settings.m_ssb, - m_settings.m_usb - ); - } - m_movingAverage.nextAverage(); + ready = true; } else if (m_settings.m_averagingMode == SpectrumSettings::AvgModeFixed) { double avg; - Real specMax = 0.0f; - if ( positiveOnly ) + if (positiveOnly) { - for (std::size_t i = 0; i < halfSize; i++) + for (int i = 0; i < halfSize; i++) { c = fftOut[i]; v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; // result available if (m_fixedAverage.storeAndGetAvg(avg, v, i)) { - m_psd[i] = avg/m_powFFTDiv; - specMax = avg > specMax ? avg : specMax; - avg = m_settings.m_linear ? avg/m_powFFTDiv : m_mult * log2fapprox(avg) + m_ofs; m_powerSpectrum[i * 2] = avg; m_powerSpectrum[i * 2 + 1] = avg; } } } - else + else if (reorder) { - for (std::size_t i = 0; i < halfSize; i++) + for (int i = 0; i < halfSize; i++) { c = fftOut[i + halfSize]; v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; // result available - if (m_fixedAverage.storeAndGetAvg(avg, v, i+halfSize)) - { - m_psd[i] = avg/m_powFFTDiv; - specMax = avg > specMax ? avg : specMax; - avg = m_settings.m_linear ? avg/m_powFFTDiv : m_mult * log2fapprox(avg) + m_ofs; + if (m_fixedAverage.storeAndGetAvg(avg, v, i+halfSize)) { m_powerSpectrum[i] = avg; } c = fftOut[i]; v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; // result available - if (m_fixedAverage.storeAndGetAvg(avg, v, i)) - { - m_psd[i + halfSize] = avg/m_powFFTDiv; - specMax = avg > specMax ? avg : specMax; - avg = m_settings.m_linear ? avg/m_powFFTDiv : m_mult * log2fapprox(avg) + m_ofs; + if (m_fixedAverage.storeAndGetAvg(avg, v, i)) { m_powerSpectrum[i + halfSize] = avg; } } } - - // result available - if (m_fixedAverage.nextAverage()) - { - m_specMax = specMax; - - // send new data to visualisation - if (m_glSpectrum) - { - m_glSpectrum->newSpectrum( - &m_powerSpectrum.data()[fftMin], - fftMax - fftMin, - m_settings.m_fftSize - ); - } - - // web socket spectrum connections - if (m_wsSpectrum.socketOpened()) - { - m_wsSpectrum.newSpectrum( - m_powerSpectrum, - m_settings.m_fftSize, - m_centerFrequency, - m_sampleRate, - m_settings.m_linear, - m_settings.m_ssb, - m_settings.m_usb - ); - } - } - } - else if (m_settings.m_averagingMode == SpectrumSettings::AvgModeMax) - { - double max; - Real specMax = 0.0f; - - if ( positiveOnly ) - { - for (std::size_t i = 0; i < halfSize; i++) - { - c = fftOut[i]; - v = c.real() * c.real() + c.imag() * c.imag(); - - // result available - if (m_max.storeAndGetMax(max, v, i)) - { - m_psd[i] = max/m_powFFTDiv; - specMax = max > specMax ? max : specMax; - max = m_settings.m_linear ? max/m_powFFTDiv : m_mult * log2fapprox(max) + m_ofs; - m_powerSpectrum[i * 2] = max; - m_powerSpectrum[i * 2 + 1] = max; - } - } - } else { - for (std::size_t i = 0; i < halfSize; i++) + for (int i = 0; i < fftSize; i++) { - c = fftOut[i + halfSize]; - v = c.real() * c.real() + c.imag() * c.imag(); - - // result available - if (m_max.storeAndGetMax(max, v, i+halfSize)) - { - m_psd[i] = max/m_powFFTDiv; - specMax = max > specMax ? max : specMax; - max = m_settings.m_linear ? max/m_powFFTDiv : m_mult * log2fapprox(max) + m_ofs; - m_powerSpectrum[i] = max; - } - c = fftOut[i]; v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; // result available - if (m_max.storeAndGetMax(max, v, i)) - { - m_psd[i + halfSize] = max/m_powFFTDiv; - specMax = max > specMax ? max : specMax; - max = m_settings.m_linear ? max/m_powFFTDiv : m_mult * log2fapprox(max) + m_ofs; - m_powerSpectrum[i + halfSize] = max; + if (m_fixedAverage.storeAndGetAvg(avg, v, i)) { + m_powerSpectrum[i] = avg; } } } // result available - if (m_max.nextMax()) - { - m_specMax = specMax; - - // send new data to visualisation - if (m_glSpectrum) - { - m_glSpectrum->newSpectrum( - &m_powerSpectrum.data()[fftMin], - fftMax - fftMin, - m_settings.m_fftSize - ); - } - - // web socket spectrum connections - if (m_wsSpectrum.socketOpened()) - { - m_wsSpectrum.newSpectrum( - m_powerSpectrum, - m_settings.m_fftSize, - m_centerFrequency, - m_sampleRate, - m_settings.m_linear, - m_settings.m_ssb, - m_settings.m_usb - ); - } + if (m_fixedAverage.nextAverage()) { + ready = true; } } -} + else if (m_settings.m_averagingMode == SpectrumSettings::AvgModeMax) + { + Real max; + + if (positiveOnly) + { + for (int i = 0; i < halfSize; i++) + { + c = fftOut[i]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + + // result available + if (m_max.storeAndGetMax(max, v, i)) + { + m_powerSpectrum[i * 2] = max; + m_powerSpectrum[i * 2 + 1] = max; + } + } + } + else if (reorder) + { + for (int i = 0; i < halfSize; i++) + { + c = fftOut[i + halfSize]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + + // result available + if (m_max.storeAndGetMax(max, v, i+halfSize)) { + m_powerSpectrum[i] = max; + } + + c = fftOut[i]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + + // result available + if (m_max.storeAndGetMax(max, v, i)) { + m_powerSpectrum[i + halfSize] = max; + } + } + } + else + { + for (int i = 0; i < fftSize; i++) + { + c = fftOut[i]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + + // result available + if (m_max.storeAndGetMax(max, v, i)) { + m_powerSpectrum[i] = max; + } + } + } + + // result available + if (m_max.nextMax()) { + ready = true; + } + } + else if (m_settings.m_averagingMode == SpectrumSettings::AvgModeMin) + { + Real min; + + if (positiveOnly) + { + for (int i = 0; i < halfSize; i++) + { + c = fftOut[i]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + + if (m_min.storeAndGetMin(min, v, i)) + { + m_powerSpectrum[i * 2] = min; + m_powerSpectrum[i * 2 + 1] = min; + } + } + } + else if (reorder) + { + for (int i = 0; i < halfSize; i++) + { + c = fftOut[i + halfSize]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + + // result available + if (m_min.storeAndGetMin(min, v, i+halfSize)) { + m_powerSpectrum[i] = min; + } + + c = fftOut[i]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + + // result available + if (m_min.storeAndGetMin(min, v, i)) { + m_powerSpectrum[i + halfSize] = min; + } + } + } + else + { + for (int i = 0; i < fftSize; i++) + { + c = fftOut[i]; + v = c.real() * c.real() + c.imag() * c.imag(); + v *= m_powFFTMul; + + if (m_min.storeAndGetMin(min, v, i)) { + m_powerSpectrum[i] = min; + } + } + } + + // result available + if (m_min.nextMin()) { + ready = true; + } + } + + if (ready) + { + for (int i = fftSize; i < m_settings.m_fftSize; i++) { + m_powerSpectrum[i] = 0.0f; + } + + // Calculate maximum value in spectrum + m_specMax = *std::max_element(&m_powerSpectrum[0], &m_powerSpectrum[m_settings.m_fftSize]); + + // Perform math operation on linear value + if (m_settings.m_mathMode != SpectrumSettings::MathModeNone) { + mathLinear(m_powerSpectrum); + } + + // Convert to dB + if (!m_settings.m_linear) + { + for (int i = 0; i < m_settings.m_fftSize; i++) { + m_powerSpectrum[i] = m_mult * log2fapprox(m_powerSpectrum[i]); + } + } + + // Perform math operation on dB value + if (m_settings.m_mathMode != SpectrumSettings::MathModeNone) { + mathDB(m_powerSpectrum); + } + + if (m_settings.mathAverageUsed()) { + m_mathMovingAverage.nextAverage(); + } + + // Stop profiling before newSpectrum, as we profile that separately + PROFILER_STOP("processFFT"); + + // send new data to visualisation + if (m_glSpectrum) + { + m_glSpectrum->newSpectrum( + &m_powerSpectrum.data()[0], + m_settings.m_fftSize + ); + } + + // web socket spectrum connections + if (m_wsSpectrum.socketOpened()) + { + m_wsSpectrum.newSpectrum( + m_powerSpectrum, + m_settings.m_fftSize, + m_centerFrequency, + m_sampleRate, + m_settings.m_linear, + m_settings.m_ssb, + m_settings.m_usb + ); + } + } + else + { + PROFILER_STOP("processFFT"); + } -void SpectrumVis::getZoomedPSDCopy(std::vector& copy) const -{ - int fftMin = (m_settings.m_frequencyZoomFactor == 1.0f) ? - 0 : (m_settings.m_frequencyZoomPos - (0.5f / m_settings.m_frequencyZoomFactor)) * m_settings.m_fftSize; - int fftMax = (m_settings.m_frequencyZoomFactor == 1.0f) ? - m_settings.m_fftSize : (m_settings.m_frequencyZoomPos + (0.5f / m_settings.m_frequencyZoomFactor)) * m_settings.m_fftSize; - copy.assign(m_psd.begin() + fftMin, m_psd.begin() + fftMax); } void SpectrumVis::start() @@ -826,13 +727,6 @@ bool SpectrumVis::handleMessage(const Message& message) MsgStartStop& cmd = (MsgStartStop&) message; setRunning(cmd.getStartStop()); return true; - } - else if (MsgFrequencyZooming::match(message)) - { - MsgFrequencyZooming& cmd = (MsgFrequencyZooming&) message; - m_settings.m_frequencyZoomFactor = cmd.getFrequencyZoomFactor(); - m_settings.m_frequencyZoomPos = cmd.getFrequencyZoomPos(); - return true; } else { @@ -876,14 +770,13 @@ void SpectrumVis::applySettings(const SpectrumSettings& settings, bool force) } m_fftEngineSequence = fftFactory->getEngine(fftSize, false, &m_fft); - m_ofs = 20.0f * log10f(1.0f / fftSize); - m_powFFTDiv = fftSize * fftSize; + m_powFFTMul = 1.0f / (fftSize * fftSize); if (fftSize > m_settings.m_fftSize) { m_fftBuffer.resize(fftSize); m_powerSpectrum.resize(fftSize); - m_psd.resize(fftSize); + m_mathMemory.resize(fftSize); } } @@ -912,6 +805,17 @@ void SpectrumVis::applySettings(const SpectrumSettings& settings, bool force) m_movingAverage.resize(fftSize, averagingValue); m_fixedAverage.resize(fftSize, averagingValue); m_max.resize(fftSize, averagingValue); + m_min.resize(fftSize, averagingValue); + } + + if ((fftSize != m_settings.m_fftSize) + || (settings.m_mathAvgCount != m_settings.m_mathAvgCount) || force) + { + m_mathMovingAverage.resize(fftSize, settings.m_mathAvgCount); + } + + if (settings.m_mathMode != m_settings.m_mathMode) { + m_mathMovingAverage.clear(); } if ((settings.m_wsSpectrumAddress != m_settings.m_wsSpectrumAddress) @@ -1085,11 +989,10 @@ void SpectrumVis::webapiUpdateSpectrumSettings( } // To calculate power, the usual equation: -// 10*log10(V1/V2), where V2=fftSize^2 +// 10*log10(v=V1/V2), where V2=fftSize^2 // is calculated using log2 instead, with: -// ofs=20.0f * log10f(1.0f / fftSize) -// mult=(10.0f / log2(10.0f)) -// dB = m_mult * log2f(v) + m_ofs +// mult = 10.0f / log2(10.0f) +// dB = m_mult * log2f(v) // However, while the gcc version of log2f is twice as fast as log10f, // MSVC version is 6x slower. // Also, we don't need full accuracy of log2f for calculating the power for the spectrum, @@ -1128,3 +1031,23 @@ float SpectrumVis::log2fapprox(float x) const return l; } + +void SpectrumVis::setMathMemory(const QList &values) +{ + QMutexLocker mutexLocker(&m_mutex); + + int s = std::min((int) values.size(), m_settings.m_fftSize); + + for (int i = 0; i < s; i++) { + m_mathMemory[i] = values[i]; + } +} + +void SpectrumVis::getMathMovingAverageCopy(QList& copy) +{ + QMutexLocker mutexLocker(&m_mutex); + + std::vector averages; + m_mathMovingAverage.getAverages(averages); + copy = QList(averages.begin(), averages.end()); +} diff --git a/sdrbase/dsp/spectrumvis.h b/sdrbase/dsp/spectrumvis.h index c5805f559..fdd727727 100644 --- a/sdrbase/dsp/spectrumvis.h +++ b/sdrbase/dsp/spectrumvis.h @@ -3,6 +3,7 @@ // written by Christian Daniel // // Copyright (C) 2015-2022 Edouard Griffiths, F4EXB // // Copyright (C) 2022 Jiří Pinkava // +// Copyright (C) 2026 Jon Beniston, M7RCE // // // // 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 // @@ -34,6 +35,7 @@ #include "util/movingaverage2d.h" #include "util/fixedaverage2d.h" #include "util/max2d.h" +#include "util/min2d.h" #include "websockets/wsspectrum.h" class GLSpectrumInterface; @@ -108,35 +110,6 @@ public: {} }; - class SDRBASE_API MsgFrequencyZooming : public Message { - MESSAGE_CLASS_DECLARATION - - public: - float getFrequencyZoomFactor() const { return m_frequencyZoomFactor; } - float getFrequencyZoomPos() const { return m_frequencyZoomPos; } - - static MsgFrequencyZooming* create(float frequencyZoomFactor, float frequencyZoomPos) { - return new MsgFrequencyZooming(frequencyZoomFactor, frequencyZoomPos); - } - - private: - float m_frequencyZoomFactor; - float m_frequencyZoomPos; - - MsgFrequencyZooming(float frequencyZoomFactor, float frequencyZoomPos) : - Message(), - m_frequencyZoomFactor(frequencyZoomFactor), - m_frequencyZoomPos(frequencyZoomPos) - { } - }; - - enum AvgMode - { - AvgModeNone, - AvgModeMovingAvg, - AvgModeFixedAvg, - AvgModeMax - }; SpectrumVis(Real scalef); virtual ~SpectrumVis(); @@ -148,19 +121,19 @@ public: void setScalef(Real scalef); void configureWSSpectrum(const QString& address, uint16_t port); const SpectrumSettings& getSettings() const { return m_settings; } - Real getSpecMax() const { return m_specMax / m_powFFTDiv; } - void getPowerSpectrumCopy(std::vector& copy) { copy.assign(m_powerSpectrum.begin(), m_powerSpectrum.end()); } - void getPSDCopy(std::vector& copy) const { copy.assign(m_psd.begin(), m_psd.begin() + m_settings.m_fftSize); } - void getZoomedPSDCopy(std::vector& copy) const; + Real getSpecMax() const { return m_specMax; } - virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool positiveOnly); + void setMathMemory(const QList &values); + void getMathMovingAverageCopy(QList& copy); + + void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool positiveOnly) override; void feed(const ComplexVector::const_iterator& begin, const ComplexVector::const_iterator& end, bool positiveOnly); - virtual void feed(const Complex *begin, unsigned int length); //!< direct FFT feed + void feed(const Complex *begin, unsigned int length) override; //!< feed output of FFT void feedTriggered(const SampleVector::const_iterator& triggerPoint, const SampleVector::const_iterator& end, bool positiveOnly); - virtual void start(); - virtual void stop(); - virtual void pushMessage(Message *msg); - virtual QString getSinkName(); + void start() override; + void stop() override; + void pushMessage(Message *msg) override; + QString getSinkName() override; MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } void setMessageQueueToGUI(MessageQueue *queue) { m_guiMessageQueue = queue; } @@ -220,7 +193,7 @@ private: std::vector m_fftBuffer; std::vector m_powerSpectrum; //!< displayable power spectrum - std::vector m_psd; //!< real PSD + std::vector m_mathMemory; SpectrumSettings m_settings; int m_overlapSize; @@ -233,22 +206,24 @@ private: WSSpectrum m_wsSpectrum; MovingAverage2D m_movingAverage; FixedAverage2D m_fixedAverage; - Max2D m_max; + Max2D m_max; + Min2D m_min; Real m_specMax; + MovingAverage2D m_mathMovingAverage; uint64_t m_centerFrequency; int m_sampleRate; - Real m_ofs; - Real m_powFFTDiv; - static const Real m_mult; + Real m_powFFTMul; //!< 1/fftSize^2 + static const Real m_mult; //!< 10/log2(10) MessageQueue m_inputMessageQueue; MessageQueue *m_guiMessageQueue; //!< Input message queue to the GUI QRecursiveMutex m_mutex; - void processFFT(bool positiveOnly); + void performFFT(bool positiveOnly); + void processFFT(const Complex* fftOut, bool reorder, bool positiveOnly, int fftSize); void setRunning(bool running) { m_running = running; } void applySettings(const SpectrumSettings& settings, bool force = false); bool handleMessage(const Message& message); @@ -257,6 +232,8 @@ private: void handleWSOpenClose(bool openClose); void handleConfigureWSSpectrum(const QString& address, uint16_t port); float log2fapprox(float x) const; + void mathLinear(std::vector &spectrum); + void mathDB(std::vector &spectrum); static void webapiFormatSpectrumSettings(SWGSDRangel::SWGGLSpectrum& response, const SpectrumSettings& settings); static void webapiUpdateSpectrumSettings( diff --git a/sdrbase/util/circularbuffer.h b/sdrbase/util/circularbuffer.h new file mode 100644 index 000000000..61f410f1a --- /dev/null +++ b/sdrbase/util/circularbuffer.h @@ -0,0 +1,200 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_CIRCULARBUFFER_H_ +#define INCLUDE_CIRCULARBUFFER_H_ + +#include +#include +#include +#include +#include + +// Iterates from oldest item to newest +template +class CircularBufferIterator { + + using iterator_category = std::forward_iterator_tag; + using value_type = T; + using pointer = T *; + using const_pointer = const T *; + using reference = T &; + using const_reference = const T &; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + +public: + CircularBufferIterator(std::vector *b, size_t tail, size_t p) : + m_buf(b), + m_tail(tail), + m_pos(p) + { + } + + value_type &operator*() { return (*m_buf)[(m_tail + m_pos) % m_buf->size()]; } + value_type *operator->() { return &(operator*()); } + + CircularBufferIterator &operator++() + { + m_pos++; + return *this; + } + + friend bool operator!= (const CircularBufferIterator& a, const CircularBufferIterator& b) { return a.m_buf != b.m_buf || a.m_tail != b.m_tail || a.m_pos != b.m_pos; }; + +private: + std::vector *m_buf; + size_type m_tail; + size_type m_pos; +}; + +// Circular buffer with fixed capacity, that overwrites oldest item when full. +// Items are stored in a contiguous block of memory, so can be accessed by index or iterated over. +// Index of 0 is the oldest item, index of size()-1 is the newest item. +template +class CircularBuffer +{ + using value_type = T; + using pointer = T *; + using const_pointer = const T *; + using reference = T &; + using const_reference = const T &; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + std::vector m_buffer; + size_type m_head; + size_type m_tail; + size_type m_count; + +public: + + CircularBuffer(size_type size) : + m_buffer(size), + m_head(0), + m_tail(0), + m_count(0) + { + } + + bool isFull() const + { + return (m_count != 0) && (m_head == m_tail); + } + + bool isEmpty() const + { + return m_count == 0; + } + + size_type size() const + { + return m_count; + } + + void clear() + { + m_head = 0; + m_tail = 0; + m_count = 0; + } + + void append(const_reference item) + { + size_type next = (m_head + 1) % m_buffer.size(); + if (isFull()) { + m_tail = next; // Overwrite oldest item + } else { + m_count++; + } + m_buffer[m_head] = item; + m_head = next; + } + + void removeFirst() + { + m_tail = (m_tail + 1) % m_buffer.size(); + m_count--; + } + + reference takeFirst() + { + reference item = m_buffer[m_tail]; + removeFirst(); + return item; + } + + // newSize of <= 1 not supported + void resize(size_type newSize) + { + // Reduce count to newSize, by removing oldest items first + while (size() > newSize) { + removeFirst(); + } + + // Shuffle data to the beginning of the buffer, so we can free up or allocate space at the end + if (m_head < m_tail) + { + std::rotate(m_buffer.begin(), m_buffer.begin() + m_tail, m_buffer.end()); + m_head = m_count; + m_tail = 0; + } + else + { + std::rotate(m_buffer.begin(), m_buffer.begin() + m_tail, m_buffer.begin() + m_head); + m_head = m_count; + m_tail = 0; + } + + m_buffer.resize(newSize); + + if (m_head >= newSize) { + m_head -= newSize; + } + } + + // Index of 0 is the oldest item, index of size()-1 is the newest item + reference operator[] (size_type index) + { + return m_buffer[(m_tail + index) % m_buffer.size()]; + } + + const_reference operator[] (size_type index) const + { + return m_buffer[(m_tail + index) % m_buffer.size()]; + } + + const_reference at(size_type index) const + { + return m_buffer.at((m_tail + index) % m_buffer.size()); + } + + using iterator = CircularBufferIterator; + + iterator begin() + { + return iterator(&m_buffer, m_tail, 0); + } + + iterator end() + { + return iterator(&m_buffer, m_tail, m_count); + } + +}; + +#endif /* INCLUDE_CIRCULARBUFFER_H_ */ diff --git a/sdrbase/util/min2d.h b/sdrbase/util/min2d.h new file mode 100644 index 000000000..c0f3eb571 --- /dev/null +++ b/sdrbase/util/min2d.h @@ -0,0 +1,108 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2018-2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2026 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRBASE_UTIL_MIN2D_H_ +#define SDRBASE_UTIL_MIN2D_H_ + +#include + +template +class Min2D +{ +public: + Min2D() : m_min(0), m_maxSize(0), m_width(0), m_size(0), m_index(0) {} + + ~Min2D() + { + if (m_min) { + delete[] m_min; + } + } + + void resize(unsigned int width, unsigned int size) + { + if (width > m_maxSize) + { + m_maxSize = width; + if (m_min) { + delete[] m_min; + } + m_min = new T[m_maxSize]; + } + + m_width = width; + m_size = size; + + std::fill(m_min, m_min+m_width, 0); + m_index = 0; + } + + bool storeAndGetMin(T& min, T v, unsigned int index) + { + if (m_size <= 1) + { + min = v; + return true; + } + + if (m_index == 0) + { + m_min[index] = v; + return false; + } + else if (m_index == m_size - 1) + { + m_min[index] = std::min(m_min[index], v); + min = m_min[index]; + return true; + } + else + { + m_min[index] = std::min(m_min[index], v); + return false; + } + } + + bool nextMin() + { + if (m_size <= 1) { + return true; + } + + if (m_index == m_size - 1) + { + m_index = 0; + std::fill(m_min, m_min+m_width, 0); + return true; + } + else + { + m_index++; + return false; + } + } + +private: + T *m_min; + unsigned int m_maxSize; + unsigned int m_width; + unsigned int m_size; + unsigned int m_index; +}; + +#endif /* SDRBASE_UTIL_MIN2D_H_ */ diff --git a/sdrbase/util/movingaverage2d.h b/sdrbase/util/movingaverage2d.h index 8358c59ac..fabfad139 100644 --- a/sdrbase/util/movingaverage2d.h +++ b/sdrbase/util/movingaverage2d.h @@ -24,7 +24,7 @@ template class MovingAverage2D { public: - MovingAverage2D() : m_data(0), m_sum(0), m_dataSize(0), m_sumSize(0), m_width(0), m_depth(0), m_avgIndex(0) {} + MovingAverage2D() : m_data(0), m_sum(0), m_dataSize(0), m_sumSize(0), m_width(0), m_depth(0), m_avgIndex(0), m_count(0) {} ~MovingAverage2D() { @@ -37,6 +37,15 @@ public: } } + void clear() + { + std::fill(m_data, m_data+(m_width*m_depth), 0.0); + std::fill(m_sum, m_sum+m_width, 0.0); + + m_avgIndex = 0; + m_count = 1; + } + void resize(unsigned int width, unsigned int depth) { if (width*depth > m_dataSize) @@ -60,10 +69,7 @@ public: m_width = width; m_depth = depth; - std::fill(m_data, m_data+(m_width*m_depth), 0.0); - std::fill(m_sum, m_sum+m_width, 0.0); - - m_avgIndex = 0; + clear(); } T storeAndGetAvg(T v, unsigned int index) @@ -77,7 +83,7 @@ public: T first = m_data[m_avgIndex*m_width+index]; m_sum[index] += (v - first); m_data[m_avgIndex*m_width+index] = v; - return m_sum[index] / m_depth; + return m_sum[index] / m_count; } else { @@ -104,8 +110,31 @@ public: } } - void nextAverage() { + void nextAverage() + { m_avgIndex = m_avgIndex == m_depth-1 ? 0 : m_avgIndex+1; + m_count = m_count == m_depth ? m_count : m_count+1; + } + + template + void getAverages(std::vector &values) const + { + values.resize(m_width); + for (unsigned int index = 0; index < m_width; index++) { + values[index] = (R) (m_sum[index] / m_count); + } + } + + T getMin() const + { + T minSum = *std::min_element(m_sum, m_sum + m_width); + return minSum / m_count; + } + + T getMax() const + { + T maxSum = *std::max_element(m_sum, m_sum + m_width); + return maxSum / m_count; } private: @@ -116,6 +145,7 @@ private: unsigned int m_width; unsigned int m_depth; unsigned int m_avgIndex; + unsigned int m_count; }; diff --git a/sdrbase/util/profiler.cpp b/sdrbase/util/profiler.cpp index 4ec5a45be..081532ce7 100644 --- a/sdrbase/util/profiler.cpp +++ b/sdrbase/util/profiler.cpp @@ -22,6 +22,7 @@ QHash GlobalProfileData::m_profileData; QMutex GlobalProfileData::m_mutex; +QElapsedTimer GlobalProfileData::m_startTimer; QHash& GlobalProfileData::getProfileData() { @@ -38,5 +39,6 @@ void GlobalProfileData::resetProfileData() { m_mutex.lock(); m_profileData.clear(); + m_startTimer.start(); m_mutex.unlock(); } diff --git a/sdrbase/util/profiler.h b/sdrbase/util/profiler.h index 2e17f23db..8bdcd9b7a 100644 --- a/sdrbase/util/profiler.h +++ b/sdrbase/util/profiler.h @@ -68,19 +68,22 @@ public: ProfileData() : m_numSamples(0), m_last(0), - m_total(0) + m_total(0), + m_max(0) { } void reset() { m_numSamples = 0; m_total = 0; + m_max = 0; } void add(qint64 sample) { m_last = sample; m_total += sample; + m_max = std::max(m_max, sample); m_numSamples++; } @@ -95,12 +98,14 @@ public: qint64 getTotal() const { return m_total; } qint64 getLast() const { return m_last; } + qint64 getMax() const { return m_max; } quint64 getNumSamples() const { return m_numSamples; } private: quint64 m_numSamples; qint64 m_last; qint64 m_total; + qint64 m_max; }; // Global thread-safe profile data that can be displayed in the GUI @@ -112,11 +117,13 @@ public: static QHash& getProfileData(); static void releaseProfileData(); static void resetProfileData(); + static qint64 getMSSinceStart() { return m_startTimer.elapsed(); } private: static QHash m_profileData; static QMutex m_mutex; + static QElapsedTimer m_startTimer; }; diff --git a/sdrbase/util/units.h b/sdrbase/util/units.h index 5d770a2c2..b4f00c268 100644 --- a/sdrbase/util/units.h +++ b/sdrbase/util/units.h @@ -2,7 +2,7 @@ // Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany // // written by Christian Daniel // // Copyright (C) 2015-2016, 2018-2019 Edouard Griffiths, F4EXB // -// Copyright (C) 2020-2023 Jon Beniston, M7RCE // +// Copyright (C) 2020-2026 Jon Beniston, M7RCE // // // // 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 // @@ -176,15 +176,15 @@ public: if (dms.captureCount() >= 1) { neg = match.capturedTexts()[1] == "-"; } - if (dms.captureCount() >= 3) { + if ((dms.captureCount() >= 3) && (match.capturedTexts().size() >= 3)) { d = match.capturedTexts()[2].toFloat(); } float m = 0.0f; - if (dms.captureCount() >= 5) { + if ((dms.captureCount() >= 5) && (match.capturedTexts().size() >= 5)) { m = match.capturedTexts()[4].toFloat(); } float s = 0.0f; - if (dms.captureCount() >= 7) { + if ((dms.captureCount() >= 7) && (match.capturedTexts().size() >= 7)) { s = match.capturedTexts()[6].toFloat(); } degrees = d + m/60.0 + s/(60.0*60.0); @@ -382,7 +382,6 @@ public: int raMins = match.capturedTexts()[2].toInt(); float raSecs = match.capturedTexts()[3].toFloat(); ra = raHours + raMins / 60.0f + raSecs / (60.0f * 60.0f); - qDebug() << ra << raHours << raMins << raSecs; int decDegs = match.capturedTexts()[5].toInt(); int decMins = match.capturedTexts()[6].toInt(); float decSecs = match.capturedTexts()[7].toFloat(); @@ -478,6 +477,68 @@ public: return T(10.0) * std::log10(tempK/refTempK+T(1.0)); } + // Convert number using engineering units to double precision. E.g: 10.3M 0.3m -5k 3.4 + static double engUnitsToDouble(const QString& string, double defaultValue = 0.0, bool *ok = nullptr) + { + static const QChar micro(0xb5); + static const QString pattern = QString(R"((-?)([\d]+)?(\.[\d]+)?([n%1umkMG]?))").arg(micro); + static const QMap map = { + {'n', 1e-9}, + {micro, 1e-6}, + {'u', 1e-6}, + {'m', 1e-3}, + {'k', 1e3}, + {'M', 1e6}, + {'G', 1e9} + }; + + QRegularExpression re(QRegularExpression::anchoredPattern(pattern)); + QRegularExpressionMatch match; + + match = re.match(string); + if (match.hasMatch()) + { + double value = (match.capturedTexts()[1] + match.capturedTexts()[2] + match.capturedTexts()[3]).toDouble(); + QString units = match.capturedTexts()[4]; + if (!units.isEmpty()) { + value = value * map.value(units[0]); + } + if (ok) { + *ok = true; + } + return value; + } + else + { + if (ok) { + *ok = false; + } + return defaultValue; + } + } + + // Convert number using engineering units to positive integer + static int engUnitsToPosInt(const QString& string, int defaultValue = 0, bool *ok = nullptr) + { + bool doubleOk; + double doubleValue = Units::engUnitsToDouble(string, 0.0, &doubleOk); + + if (doubleOk && (doubleValue >= 0.0) && (trunc(doubleValue) == doubleValue)) + { + if (ok) { + *ok = true; + } + return (int) doubleValue; + } + else + { + if (ok) { + *ok = false; + } + return defaultValue; + } + } + }; #endif // INCLUDE_UNITS_H diff --git a/sdrgui/CMakeLists.txt b/sdrgui/CMakeLists.txt index 321474cfb..79da6528e 100644 --- a/sdrgui/CMakeLists.txt +++ b/sdrgui/CMakeLists.txt @@ -87,6 +87,7 @@ set(sdrgui_SOURCES gui/scidoublespinbox.cpp gui/sdrangelsplash.cpp gui/spectrumcalibrationpointsdialog.cpp + gui/spectrumdisplaysettingsdialog.cpp gui/spectrummarkersdialog.cpp gui/spectrummeasurementsdialog.cpp gui/spectrummeasurements.cpp @@ -221,6 +222,7 @@ set(sdrgui_HEADERS gui/scidoublespinbox.h gui/sdrangelsplash.h gui/spectrumcalibrationpointsdialog.h + gui/spectrumdisplaysettingsdialog.h gui/spectrummarkersdialog.h gui/spectrummeasurementsdialog.h gui/spectrummeasurements.h @@ -301,6 +303,7 @@ set(sdrgui_FORMS gui/spectrummarkersdialog.ui gui/spectrummeasurementsdialog.ui gui/spectrumcalibrationpointsdialog.ui + gui/spectrumdisplaysettingsdialog.ui gui/myposdialog.ui gui/transverterdialog.ui gui/loggingdialog.ui diff --git a/sdrgui/gui/framelesswindowresizer.cpp b/sdrgui/gui/framelesswindowresizer.cpp index 82e5bba58..6485cd5d7 100644 --- a/sdrgui/gui/framelesswindowresizer.cpp +++ b/sdrgui/gui/framelesswindowresizer.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include "framelesswindowresizer.h" @@ -55,6 +56,11 @@ void FramelessWindowResizer::enableChildMouseTracking() table->viewport()->setMouseTracking(true); table->viewport()->installEventFilter(this); } + // Likewise for scroll bars, such as in GLSpectrum + QList scrollBars = m_widget->findChildren(); + for (auto scrollBar : scrollBars) { + scrollBar->installEventFilter(this); + } } bool FramelessWindowResizer::mouseOnTopBorder(QPoint pos) const diff --git a/sdrgui/gui/glshaderspectrogram.cpp b/sdrgui/gui/glshaderspectrogram.cpp index a79428fe2..c1e46f63a 100644 --- a/sdrgui/gui/glshaderspectrogram.cpp +++ b/sdrgui/gui/glshaderspectrogram.cpp @@ -77,7 +77,7 @@ GLShaderSpectrogram::GLShaderSpectrogram() : m_lightRotX(0.0), m_lightRotY(0.0), m_lightRotZ(0.0), - m_gridElements(1024) + m_gridElements(0) { } @@ -154,7 +154,11 @@ void GLShaderSpectrogram::initializeGL(int majorVersion, int minorVersion) void GLShaderSpectrogram::initGrid(int elements) { - m_gridElements = std::min(elements, 4096); // Limit to keep memory requirements realistic + int gridElements = std::min(elements, 4096); // Limit to keep memory requirements realistic + if (gridElements == m_gridElements) { + return; + } + m_gridElements = gridElements; qDebug() << "GLShaderSpectrogram::initGrid: requested: " << elements << " actual: " << m_gridElements; int e1 = m_gridElements+1; diff --git a/sdrgui/gui/glspectrum.cpp b/sdrgui/gui/glspectrum.cpp index cd540b241..d0d17df90 100644 --- a/sdrgui/gui/glspectrum.cpp +++ b/sdrgui/gui/glspectrum.cpp @@ -33,11 +33,21 @@ GLSpectrum::GLSpectrum(QWidget *parent) : QWidget(parent) { - m_splitter = new QSplitter(Qt::Vertical); + m_spectrumContainer = new QWidget(); + QHBoxLayout *hLayout = new QHBoxLayout(m_spectrumContainer); m_spectrum = new GLSpectrumView(); + m_scrollBar = new QScrollBar(Qt::Vertical); + m_spectrum->setScrollBar(m_scrollBar); + hLayout->addWidget(m_spectrum); + hLayout->addWidget(m_scrollBar); + hLayout->setContentsMargins(0, 0, 0, 0); + hLayout->setSpacing(0); + m_measurements = new SpectrumMeasurements(); m_spectrum->setMeasurements(m_measurements); - m_splitter->addWidget(m_spectrum); + + m_splitter = new QSplitter(Qt::Vertical); + m_splitter->addWidget(m_spectrumContainer); m_splitter->addWidget(m_measurements); m_position = SpectrumSettings::PositionBelow; QVBoxLayout *layout = new QVBoxLayout(this); @@ -63,7 +73,7 @@ void GLSpectrum::setMeasurementsPosition(SpectrumSettings::MeasurementsPosition break; case SpectrumSettings::PositionBelow: m_splitter->setOrientation(Qt::Vertical); - m_splitter->insertWidget(0, m_spectrum); + m_splitter->insertWidget(0, m_spectrumContainer); break; case SpectrumSettings::PositionLeft: m_splitter->setOrientation(Qt::Horizontal); @@ -71,7 +81,7 @@ void GLSpectrum::setMeasurementsPosition(SpectrumSettings::MeasurementsPosition break; case SpectrumSettings::PositionRight: m_splitter->setOrientation(Qt::Horizontal); - m_splitter->insertWidget(0, m_spectrum); + m_splitter->insertWidget(0, m_spectrumContainer); break; } m_position = position; @@ -79,9 +89,9 @@ void GLSpectrum::setMeasurementsPosition(SpectrumSettings::MeasurementsPosition void GLSpectrum::setMeasurementParams(SpectrumSettings::Measurement measurement, int centerFrequencyOffset, int bandwidth, int chSpacing, int adjChBandwidth, - int harmonics, int peaks, bool highlight, int precision) + int harmonics, int peaks, bool highlight, int precision, unsigned memoryMask) { - m_spectrum->setMeasurementParams(measurement, centerFrequencyOffset, bandwidth, chSpacing, adjChBandwidth, harmonics, peaks, highlight, precision); + m_spectrum->setMeasurementParams(measurement, centerFrequencyOffset, bandwidth, chSpacing, adjChBandwidth, harmonics, peaks, highlight, precision, memoryMask); // Resize splitter so there's just enough space for the measurements table // But don't use more than 50% QList sizes = m_splitter->sizes(); @@ -148,3 +158,9 @@ void GLSpectrum::setMeasurementParams(SpectrumSettings::Measurement measurement, m_splitter->setSizes(sizes); //resize(size().expandedTo(minimumSizeHint())); } + +void GLSpectrum::resetMeasurements() +{ + m_measurements->reset(); + m_spectrum->resetMeasurements(); +} diff --git a/sdrgui/gui/glspectrum.h b/sdrgui/gui/glspectrum.h index e877212a0..a01c3dd69 100644 --- a/sdrgui/gui/glspectrum.h +++ b/sdrgui/gui/glspectrum.h @@ -31,7 +31,7 @@ class QSplitter; class SpectrumMeasurements; -// Combines GLSpectrumView with SpectrumMeasurements in a QSplitter +// Combines GLSpectrumView with SpectrumMeasurements in a QSplitter and optional QScrollBar class SDRGUI_API GLSpectrum : public QWidget, public GLSpectrumInterface { Q_OBJECT @@ -50,13 +50,16 @@ public: void setTimingRate(qint32 timingRate) { m_spectrum->setTimingRate(timingRate); } void setFFTOverlap(int overlap) { m_spectrum->setFFTOverlap(overlap); } void setReferenceLevel(Real referenceLevel) { m_spectrum->setReferenceLevel(referenceLevel); } + void setReferenceLevelRange(Real minReferenceLevel, Real maxReferenceLevel) { m_spectrum->setReferenceLevelRange(minReferenceLevel, maxReferenceLevel); } void setPowerRange(Real powerRange){ m_spectrum->setPowerRange(powerRange); } + void setPowerRangeRange(Real minPowerRange, Real maxPowerRange) { m_spectrum->setPowerRangeRange(minPowerRange, maxPowerRange); } void setDecay(int decay) { m_spectrum->setDecay(decay); } void setDecayDivisor(int decayDivisor) { m_spectrum->setDecayDivisor(decayDivisor); } void setHistoStroke(int stroke) { m_spectrum->setHistoStroke(stroke); } void setDisplayWaterfall(bool display) { m_spectrum->setDisplayWaterfall(display); } void setDisplay3DSpectrogram(bool display) { m_spectrum->setDisplay3DSpectrogram(display); } void set3DSpectrogramStyle(SpectrumSettings::SpectrogramStyle style) { m_spectrum->set3DSpectrogramStyle(style); } + void setSpectrumColor(QRgb color) { m_spectrum->setSpectrumColor(color); } void setColorMapName(const QString &colorMapName) { m_spectrum->setColorMapName(colorMapName); } void setSpectrumStyle(SpectrumSettings::SpectrumStyle style) { m_spectrum->setSpectrumStyle(style); } void setSsbSpectrum(bool ssbSpectrum) { m_spectrum->setSsbSpectrum(ssbSpectrum); } @@ -73,12 +76,13 @@ public: void setUseCalibration(bool useCalibration) { m_spectrum->setUseCalibration(useCalibration); } void setMeasurementParams(SpectrumSettings::Measurement measurement, int centerFrequencyOffset, int bandwidth, int chSpacing, int adjChBandwidth, - int harmonics, int peaks, bool highlight, int precision); + int harmonics, int peaks, bool highlight, int precision, unsigned memoryMask); + void resetMeasurements(); qint32 getSampleRate() const { return m_spectrum->getSampleRate(); } void addChannelMarker(ChannelMarker* channelMarker) { m_spectrum->addChannelMarker(channelMarker); } void removeChannelMarker(ChannelMarker* channelMarker) { m_spectrum->removeChannelMarker(channelMarker); } void setMessageQueueToGUI(MessageQueue* messageQueue) { m_spectrum->setMessageQueueToGUI(messageQueue); } - void newSpectrum(const Real* spectrum, int nbBins, int fftSize) { m_spectrum->newSpectrum(spectrum, nbBins, fftSize); } + void newSpectrum(const Real* spectrum, int fftSize) { m_spectrum->newSpectrum(spectrum, fftSize); } void clearSpectrumHistogram() { m_spectrum->clearSpectrumHistogram(); } Real getWaterfallShare() const { return m_spectrum->getWaterfallShare(); } void setWaterfallShare(Real waterfallShare) { m_spectrum->setWaterfallShare(waterfallShare); } @@ -111,12 +115,22 @@ public: void setIsDeviceSpectrum(bool isDeviceSpectrum) { m_spectrum->setIsDeviceSpectrum(isDeviceSpectrum); } bool isDeviceSpectrum() const { return m_spectrum->isDeviceSpectrum(); } void setFrequencyZooming(float frequencyZoomFactor, float frequencyZoomPos) { m_spectrum->setFrequencyZooming(frequencyZoomFactor, frequencyZoomPos); } + void setScrolling(bool enabled, int length) { m_spectrum->setScrolling(enabled, length); } + void setWaterfallTimeFormat(SpectrumSettings::WaterfallTimeUnits waterfallTimeUnits, const QString& format) { m_spectrum->setWaterfallTimeFormat(waterfallTimeUnits, format); } + void setStatusLine(bool displayRBW, bool displayCursorStats, bool displayPeakStats) { m_spectrum->setStatusLine(displayRBW, displayCursorStats, displayPeakStats); } + void readCSV(QTextStream &in, bool append, QString &error) { m_spectrum->readCSV(in, append, error); } + void writeCSV(QTextStream &out) { m_spectrum->writeCSV(out); } + bool writeImage(const QString& filename) { return m_spectrum->writeImage(filename); } + void getDisplayedSpectrumCopy(std::vector& copy, bool zoomed) const { m_spectrum->getDisplayedSpectrumCopy(copy, zoomed); } + void setMemory(int memoryIdx, const SpectrumSettings::SpectrumMemory &memory) { m_spectrum->setMemory(memoryIdx, memory); } private: QSplitter *m_splitter; GLSpectrumView *m_spectrum; SpectrumMeasurements *m_measurements; SpectrumSettings::MeasurementsPosition m_position; + QWidget *m_spectrumContainer; + QScrollBar *m_scrollBar; }; diff --git a/sdrgui/gui/glspectrumgui.cpp b/sdrgui/gui/glspectrumgui.cpp index d747818ee..1c516c1a8 100644 --- a/sdrgui/gui/glspectrumgui.cpp +++ b/sdrgui/gui/glspectrumgui.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -42,11 +43,14 @@ #include "gui/spectrumcalibrationpointsdialog.h" #include "gui/spectrummeasurementsdialog.h" #include "gui/spectrummeasurements.h" +#include "gui/spectrumdisplaysettingsdialog.h" #include "gui/flowlayout.h" #include "gui/dialogpositioner.h" #include "gui/dialpopup.h" #include "util/colormap.h" #include "util/db.h" +#include "util/csv.h" +#include "util/units.h" #include "ui_glspectrumgui.h" const int GLSpectrumGUI::m_fpsMs[] = {500, 200, 100, 50, 20, 10, 5}; @@ -58,11 +62,13 @@ GLSpectrumGUI::GLSpectrumGUI(QWidget* parent) : m_glSpectrum(nullptr), m_doApplySettings(true), m_calibrationShiftdB(0.0), - m_markersDialog(nullptr) + m_markersDialog(nullptr), + m_contextMenu(nullptr) { ui->setupUi(this); // Use the custom flow layout for the 3 main horizontal layouts (lines) + ui->verticalLayout->removeItem(ui->Line7Layout); ui->verticalLayout->removeItem(ui->Line6Layout); ui->verticalLayout->removeItem(ui->Line5Layout); ui->verticalLayout->removeItem(ui->Line4Layout); @@ -76,6 +82,7 @@ GLSpectrumGUI::GLSpectrumGUI(QWidget* parent) : flowLayout->addItem(ui->Line4Layout); flowLayout->addItem(ui->Line5Layout); flowLayout->addItem(ui->Line6Layout); + flowLayout->addItem(ui->Line7Layout); ui->verticalLayout->addItem(flowLayout); on_linscale_toggled(false); @@ -101,6 +108,14 @@ GLSpectrumGUI::GLSpectrumGUI(QWidget* parent) : CRightClickEnabler *calibrationPointsRightClickEnabler = new CRightClickEnabler(ui->calibration); connect(calibrationPointsRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(openCalibrationPointsDialog(const QPoint &))); + m_contextMenu = new QMenu(this); + ui->mem1->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->mem1, &QWidget::customContextMenuRequested, this, &GLSpectrumGUI::mem1ContextMenu); + ui->mem2->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->mem2, &QWidget::customContextMenuRequested, this, &GLSpectrumGUI::mem2ContextMenu); + + connect(ui->mathAvgCount->lineEdit(), &QLineEdit::editingFinished, this, &GLSpectrumGUI::on_mathAvgCount_editingFinished); + DialPopup::addPopupsToChildDials(this); displaySettings(); @@ -178,7 +193,8 @@ void GLSpectrumGUI::updateSettings() void GLSpectrumGUI::displaySettings() { blockApplySettings(true); - ui->showAllControls->setChecked(m_settings.m_showAllControls); + ui->showControls->setCurrentIndex((int) m_settings.m_showControls); + setPowerAndRefRange(); ui->refLevel->setValue(m_settings.m_refLevel + m_calibrationShiftdB); ui->levelRange->setValue(m_settings.m_powerRange); ui->decay->setSliderPosition(m_settings.m_decay); @@ -187,7 +203,7 @@ void GLSpectrumGUI::displaySettings() ui->waterfall->setChecked(m_settings.m_displayWaterfall); ui->spectrogram->setChecked(m_settings.m_display3DSpectrogram); ui->spectrogramStyle->setCurrentIndex((int) m_settings.m_3DSpectrogramStyle); - ui->spectrogramStyle->setVisible(m_settings.m_display3DSpectrogram && m_settings.m_showAllControls); + ui->spectrogramStyle->setVisible(m_settings.m_display3DSpectrogram && (m_settings.m_showControls == SpectrumSettings::ShowAll)); ui->colorMap->setCurrentText(m_settings.m_colorMap); ui->currentLine->blockSignals(true); ui->currentFill->blockSignals(true); @@ -215,6 +231,8 @@ void GLSpectrumGUI::displaySettings() ui->averaging->blockSignals(true); ui->averagingMode->blockSignals(true); ui->linscale->blockSignals(true); + ui->mathMode->blockSignals(true); + ui->mathAvgCount->blockSignals(true); ui->fftWindow->setCurrentIndex(m_settings.m_fftWindow); @@ -245,15 +263,28 @@ void GLSpectrumGUI::displaySettings() ui->averagingMode->setCurrentIndex((int) m_settings.m_averagingMode); ui->linscale->setChecked(m_settings.m_linear); setAveragingToolitp(); + ui->mathMode->setCurrentIndex((int) m_settings.m_mathMode); + int idx = findEquivalentText(ui->mathAvgCount, m_settings.m_mathAvgCount); + if (idx >= 0) { + ui->mathAvgCount->setCurrentIndex(idx); + } else { + ui->mathAvgCount->setCurrentText(QString::number(m_settings.m_mathAvgCount)); + } + ui->mathAvgCount->setVisible(m_settings.mathAverageUsed()); ui->calibration->setChecked(m_settings.m_useCalibration); ui->resetMeasurements->setVisible(m_settings.m_measurement >= SpectrumSettings::MeasurementChannelPower); displayGotoMarkers(); displayControls(); + ui->mem1->setChecked(m_settings.m_spectrumMemory[0].m_display); + ui->mem2->setChecked(m_settings.m_spectrumMemory[1].m_display); + ui->fftWindow->blockSignals(false); ui->averaging->blockSignals(false); ui->averagingMode->blockSignals(false); ui->linscale->blockSignals(false); + ui->mathMode->blockSignals(false); + ui->mathAvgCount->blockSignals(false); blockApplySettings(false); updateMeasurements(); @@ -261,33 +292,40 @@ void GLSpectrumGUI::displaySettings() void GLSpectrumGUI::displayControls() { - ui->grid->setVisible(m_settings.m_showAllControls); - ui->gridIntensity->setVisible(m_settings.m_showAllControls); - ui->truncateScale->setVisible(m_settings.m_showAllControls); - ui->clearSpectrum->setVisible(m_settings.m_showAllControls); - ui->histogram->setVisible(m_settings.m_showAllControls); - ui->maxHold->setVisible(m_settings.m_showAllControls); - ui->decay->setVisible(m_settings.m_showAllControls); - ui->decayDivisor->setVisible(m_settings.m_showAllControls); - ui->stroke->setVisible(m_settings.m_showAllControls); - ui->currentLine->setVisible(m_settings.m_showAllControls); - ui->currentFill->setVisible(m_settings.m_showAllControls); - ui->currentGradient->setVisible(m_settings.m_showAllControls); - ui->traceIntensity->setVisible(m_settings.m_showAllControls); - ui->colorMap->setVisible(m_settings.m_showAllControls); - ui->invertWaterfall->setVisible(m_settings.m_showAllControls); - ui->waterfall->setVisible(m_settings.m_showAllControls); - ui->spectrogram->setVisible(m_settings.m_showAllControls); - ui->spectrogramStyle->setVisible(m_settings.m_showAllControls); - ui->fftWindow->setVisible(m_settings.m_showAllControls); - ui->fftSize->setVisible(m_settings.m_showAllControls); - ui->fftOverlap->setVisible(m_settings.m_showAllControls); - ui->fps->setVisible(m_settings.m_showAllControls); - ui->linscale->setVisible(m_settings.m_showAllControls); - ui->save->setVisible(m_settings.m_showAllControls); - ui->wsSpectrum->setVisible(m_settings.m_showAllControls); - ui->calibration->setVisible(m_settings.m_showAllControls); - ui->markers->setVisible(m_settings.m_showAllControls); + ui->grid->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->gridIntensity->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->truncateScale->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->clearSpectrum->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->histogram->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->maxHold->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->decay->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->decayDivisor->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->stroke->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->currentLine->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->currentFill->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->currentGradient->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->traceIntensity->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->colorMap->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->invertWaterfall->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->waterfall->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->spectrogram->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->spectrogramStyle->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll && m_settings.m_display3DSpectrogram); + ui->fftWindow->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->fftSize->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->fftOverlap->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->mathMode->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->mathAvgCount->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll && m_settings.mathAverageUsed()); + ui->fps->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->linscale->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->mem1->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->mem2->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->load->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->save->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->saveImage->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->wsSpectrum->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowAll); + ui->calibration->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->markers->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); + ui->measure->setVisible(m_settings.m_showControls >= SpectrumSettings::ShowStandard); } void GLSpectrumGUI::displayGotoMarkers() @@ -348,6 +386,7 @@ void GLSpectrumGUI::applySpectrumSettings() m_glSpectrum->setDisplayWaterfall(m_settings.m_displayWaterfall); m_glSpectrum->setDisplay3DSpectrogram(m_settings.m_display3DSpectrogram); m_glSpectrum->set3DSpectrogramStyle(m_settings.m_3DSpectrogramStyle); + m_glSpectrum->setSpectrumColor(m_settings.m_spectrumColor); m_glSpectrum->setColorMapName(m_settings.m_colorMap); m_glSpectrum->setSpectrumStyle(m_settings.m_spectrumStyle); m_glSpectrum->setInvertedWaterfall(m_settings.m_invertedWaterfall); @@ -374,7 +413,9 @@ void GLSpectrumGUI::applySpectrumSettings() Real powerRange = m_settings.m_linear ? pow(10.0, m_settings.m_refLevel/10.0) : m_settings.m_powerRange; qDebug("GLSpectrumGUI::applySpectrumSettings: refLevel: %e powerRange: %e", refLevel, powerRange); m_glSpectrum->setReferenceLevel(refLevel); + m_glSpectrum->setReferenceLevelRange(ui->refLevel->minimum(), ui->refLevel->maximum()); m_glSpectrum->setPowerRange(powerRange); + m_glSpectrum->setPowerRangeRange(ui->levelRange->minimum(), ui->levelRange->maximum()); m_glSpectrum->setFPSPeriodMs(m_settings.m_fpsPeriodMs); m_glSpectrum->setFreqScaleTruncationMode(m_settings.m_truncateFreqScale); m_glSpectrum->setLinear(m_settings.m_linear); @@ -386,6 +427,41 @@ void GLSpectrumGUI::applySpectrumSettings() m_glSpectrum->setMarkersDisplay(m_settings.m_markersDisplay); m_glSpectrum->setCalibrationPoints(m_settings.m_calibrationPoints); m_glSpectrum->setCalibrationInterpMode(m_settings.m_calibrationInterpMode); + + m_glSpectrum->setScrolling(m_settings.m_scrollBar, m_settings.m_scrollLength); + m_glSpectrum->setWaterfallTimeFormat(m_settings.m_waterfallTimeUnits, m_settings.m_waterfallTimeFormat); + m_glSpectrum->setStatusLine(m_settings.m_displayRBW, m_settings.m_displayCursorStats, m_settings.m_displayPeakStats); + + for (int i = 0; i < m_settings.m_spectrumMemory.size(); i++) { + m_glSpectrum->setMemory(i, m_settings.m_spectrumMemory[i]); + } +} + +void GLSpectrumGUI::setPowerAndRefRange() +{ + const Real maxSignal = 120.0f; // 20-bits + + if ( (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusAvgDB) + || (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM1DB) + || (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM2DB) + ) + { + ui->levelRange->setRange(1.0f, 2.0f * maxSignal); + ui->refLevel->setRange(-maxSignal, maxSignal); + } + else if ( (m_settings.m_mathMode == SpectrumSettings::MathModeAbsXMinusAvgDB) + || (m_settings.m_mathMode == SpectrumSettings::MathModeAbsXMinusM1DB) + || (m_settings.m_mathMode == SpectrumSettings::MathModeAbsXMinusM2DB) + ) + { + ui->levelRange->setRange(1.0f, maxSignal); + ui->refLevel->setRange(0.0f, maxSignal); + } + else + { + ui->levelRange->setRange(1.0f, maxSignal); + ui->refLevel->setRange(-maxSignal, 0.0f); + } } void GLSpectrumGUI::on_fftWindow_currentIndexChanged(int index) @@ -419,12 +495,14 @@ void GLSpectrumGUI::on_autoscale_clicked(bool checked) { (void) checked; - if (!m_spectrumVis) { + // Autoscale according to currently displayed spectrum (which may be old, if scrolling enabled) + + if (!m_glSpectrum) { return; } std::vector psd; - m_spectrumVis->getZoomedPSDCopy(psd); + m_glSpectrum->getDisplayedSpectrumCopy(psd, true); int avgRange = m_settings.m_fftSize / 32; if (psd.size() < (unsigned int) avgRange) { @@ -440,8 +518,18 @@ void GLSpectrumGUI::on_autoscale_clicked(bool checked) } float minAvg = minSum / avgRange; - int minLvl = CalcDb::dbPower(minAvg*2); - int maxLvl = CalcDb::dbPower(max*10); + int minLvl; + int maxLvl; + if (m_settings.m_linear) + { + minLvl = CalcDb::dbPower(minAvg*2); + maxLvl = CalcDb::dbPower(max*10); + } + else + { + minLvl = minAvg + 3; + maxLvl = max + 10; + } m_settings.m_refLevel = maxLvl; m_settings.m_powerRange = maxLvl - minLvl; @@ -458,9 +546,7 @@ void GLSpectrumGUI::on_averagingMode_currentIndexChanged(int index) qDebug("GLSpectrumGUI::on_averagingMode_currentIndexChanged: %d", index); m_settings.m_averagingMode = index < 0 ? SpectrumSettings::AvgModeNone : - index > 3 ? - SpectrumSettings::AvgModeMax : - (SpectrumSettings::AveragingMode) index; + (SpectrumSettings::AveragingMode) index; setAveragingCombo(); applySettings(); @@ -475,6 +561,30 @@ void GLSpectrumGUI::on_averaging_currentIndexChanged(int index) setAveragingToolitp(); } +void GLSpectrumGUI::on_mathMode_currentIndexChanged(int index) +{ + m_settings.m_mathMode = (SpectrumSettings::MathMode) index; + ui->mathAvgCount->setVisible((m_settings.m_showControls >= SpectrumSettings::ShowAll) && m_settings.mathAverageUsed()); + setMathMemory(); + applySettings(); +} + +void GLSpectrumGUI::on_mathAvgCount_currentIndexChanged(int index) +{ + (void) index; + + int value = Units::engUnitsToPosInt(ui->mathAvgCount->currentText(), 2); + m_settings.m_mathAvgCount = std::min(std::max(2, value), 1000000); + applySettings(); +} + +void GLSpectrumGUI::on_mathAvgCount_editingFinished() +{ + int value = Units::engUnitsToPosInt(ui->mathAvgCount->currentText(), 2); + m_settings.m_mathAvgCount = std::min(std::max(2, value), 1000000); + applySettings(); +} + void GLSpectrumGUI::on_linscale_toggled(bool checked) { qDebug("GLSpectrumGUI::on_averaging_currentIndexChanged: %s", checked ? "lin" : "log"); @@ -543,43 +653,71 @@ void GLSpectrumGUI::closeMarkersDialog() m_markersDialog = nullptr; } +// Load spectrum data to a CSV file +void GLSpectrumGUI::on_load_clicked(bool checked) +{ + (void) checked; + + QString filename = QFileDialog::getSaveFileName(this, "Select file to read data from", "", "*.csv"); + if (!filename.isEmpty()) + { + QFile file(filename); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QTextStream in(&file); + QString error; + + // Read in all data + m_glSpectrum->readCSV(in, false, error); + + if (!error.isEmpty()) { + QMessageBox::critical(this, "Spectrum", QString("Failed to read file %1: %2").arg(filename).arg(error)); + } + + file.close(); + } + else + { + QMessageBox::critical(this, "Spectrum", QString("Failed to open file %1").arg(filename)); + } + } +} + // Save spectrum data to a CSV file void GLSpectrumGUI::on_save_clicked(bool checked) { (void) checked; - // Get filename to write - QFileDialog fileDialog(nullptr, "Select file to save data to", "", "*.csv"); - fileDialog.setDefaultSuffix("csv"); - fileDialog.setAcceptMode(QFileDialog::AcceptSave); - if (fileDialog.exec()) + QString filename = QFileDialog::getSaveFileName(this, "Select file to save data to", "", "*.csv"); + if (!filename.isEmpty()) { - QStringList fileNames = fileDialog.selectedFiles(); - if (fileNames.size() > 0) + QFile file(filename); + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) { - // Get spectrum data (This vector can be larger than fftSize) - std::vector spectrum; - m_spectrumVis->getPowerSpectrumCopy(spectrum); + QTextStream out(&file); - // Write to text file - QFile file(fileNames[0]); - if (file.open(QIODevice::WriteOnly)) - { - QTextStream out(&file); - float frequency = m_glSpectrum->getCenterFrequency() - (m_glSpectrum->getSampleRate() / 2.0f); - float rbw = m_glSpectrum->getSampleRate() / (float)m_settings.m_fftSize; - out << "\"Frequency\",\"Power\"\n"; - for (int i = 0; i < m_settings.m_fftSize; i++) - { - out << frequency << "," << spectrum[i] << "\n"; - frequency += rbw; - } - file.close(); - } - else - { - QMessageBox::critical(this, "Spectrum", QString("Failed to open file %1").arg(fileNames[0])); - } + // Write out all data in scroll buffer + m_glSpectrum->writeCSV(out); + + file.close(); + } + else + { + QMessageBox::critical(this, "Spectrum", QString("Failed to open file %1").arg(filename)); + } + } +} + +// Save spectrum/waterfall image to file +void GLSpectrumGUI::on_saveImage_clicked(bool checked) +{ + (void) checked; + + QString filename = QFileDialog::getSaveFileName(this, "Select file to save image to", "", "*.jpg *.png"); + if (!filename.isEmpty()) + { + if (!m_glSpectrum->writeImage(filename)) { + QMessageBox::critical(this, "Spectrum", QString("Failed to save image to file %1").arg(filename)); } } } @@ -658,7 +796,7 @@ void GLSpectrumGUI::on_spectrogram_toggled(bool checked) ui->waterfall->setChecked(false); blockApplySettings(false); } - ui->spectrogramStyle->setVisible(m_settings.m_display3DSpectrogram && m_settings.m_showAllControls); + ui->spectrogramStyle->setVisible(m_settings.m_display3DSpectrogram && (m_settings.m_showControls >= SpectrumSettings::ShowAll)); applySettings(); } @@ -719,9 +857,9 @@ void GLSpectrumGUI::on_invertWaterfall_toggled(bool checked) applySettings(); } -void GLSpectrumGUI::on_showAllControls_toggled(bool checked) +void GLSpectrumGUI::on_showControls_currentIndexChanged(int index) { - m_settings.m_showAllControls = checked; + m_settings.m_showControls = (SpectrumSettings::ShowControls) index; displayControls(); applySettings(); } @@ -1000,9 +1138,9 @@ bool GLSpectrumGUI::handleMessage(const Message& message) } return true; } - else if (SpectrumVis::MsgFrequencyZooming::match(message)) + else if (GLSpectrumView::MsgFrequencyZooming::match(message)) { - const SpectrumVis::MsgFrequencyZooming& report = (const SpectrumVis::MsgFrequencyZooming&) message; + const GLSpectrumView::MsgFrequencyZooming& report = (const GLSpectrumView::MsgFrequencyZooming&) message; m_settings.m_frequencyZoomFactor = report.getFrequencyZoomFactor(); m_settings.m_frequencyZoomPos = report.getFrequencyZoomPos(); return true; @@ -1133,7 +1271,7 @@ void GLSpectrumGUI::on_resetMeasurements_clicked(bool checked) (void) checked; if (m_glSpectrum) { - m_glSpectrum->getMeasurements()->reset(); + m_glSpectrum->resetMeasurements(); } } @@ -1153,7 +1291,385 @@ void GLSpectrumGUI::updateMeasurements() m_settings.m_measurementHarmonics, m_settings.m_measurementPeaks, m_settings.m_measurementHighlight, - m_settings.m_measurementPrecision + m_settings.m_measurementPrecision, + m_settings.m_measurementMemMasks ); } } + +void GLSpectrumGUI::on_showSettingsDialog_clicked(bool checked) +{ + (void) checked; + + SpectrumDisplaySettingsDialog dialog(m_glSpectrum, &m_settings, m_glSpectrum->getSampleRate(), this); + + if (dialog.exec() == QDialog::Accepted) { + applySpectrumSettings(); + } +} + +void GLSpectrumGUI::on_mem1_clicked(bool checked) +{ + m_settings.m_spectrumMemory[0].m_display = checked; + if (m_glSpectrum) { + m_glSpectrum->setMemory(0, m_settings.m_spectrumMemory[0]); + } +} + +void GLSpectrumGUI::on_mem2_clicked(bool checked) +{ + m_settings.m_spectrumMemory[1].m_display = checked; + if (m_glSpectrum) { + m_glSpectrum->setMemory(1, m_settings.m_spectrumMemory[1]); + } +} + +void GLSpectrumGUI::mem1ContextMenu(const QPoint &p) +{ + memContextMenu(0, ui->mem1->mapToGlobal(p)); +} + +void GLSpectrumGUI::mem2ContextMenu(const QPoint &p) +{ + memContextMenu(1, ui->mem2->mapToGlobal(p)); +} + +// Show memory context menu +void GLSpectrumGUI::memContextMenu(int memoryIdx, const QPoint &p) +{ + if (m_glSpectrum && m_spectrumVis) + { + m_contextMenu->clear(); + + bool memNotEmpty = m_settings.m_spectrumMemory[memoryIdx].m_spectrum.size() > 0; + + // Clear memory + QAction* clearAction = new QAction(QString("Clear M%1").arg(memoryIdx+1), m_contextMenu); + connect(clearAction, &QAction::triggered, this, [this, memoryIdx]()->void { + m_settings.m_spectrumMemory[memoryIdx].m_spectrum.clear(); + updateMem(memoryIdx); + }); + m_contextMenu->addAction(clearAction); + clearAction->setEnabled(memNotEmpty); + + // Copy currently displayed spectrum to memory + QAction* copyCurrentSpectrumAction = new QAction(QString("Set M%1 to current spectrum").arg(memoryIdx+1), m_contextMenu); + connect(copyCurrentSpectrumAction, &QAction::triggered, this, [this, memoryIdx]()->void { + std::vector copy; + m_glSpectrum->getDisplayedSpectrumCopy(copy, false); + m_settings.m_spectrumMemory[memoryIdx].m_spectrum = QList(copy.begin(), copy.end()); + updateMem(memoryIdx); + }); + m_contextMenu->addAction(copyCurrentSpectrumAction); + + // Copy math moving average to memory + QAction* copyMovingAverageAction = new QAction(QString("Set M%1 to moving average").arg(memoryIdx+1), m_contextMenu); + connect(copyMovingAverageAction, &QAction::triggered, this, [this, memoryIdx]()->void { + QList copy; + bool mathDB = m_settings.mathUsesDB(); + m_spectrumVis->getMathMovingAverageCopy(copy); + convertSpectrum(copy, mathDB, !m_settings.m_linear); + m_settings.m_spectrumMemory[memoryIdx].m_spectrum = QList(copy.begin(), copy.end()); + updateMem(memoryIdx); + }); + m_contextMenu->addAction(copyMovingAverageAction); + copyMovingAverageAction->setEnabled(m_settings.mathAverageUsed()); + + // Add offset + QAction* applyOffsetAction = new QAction(QString("Add offset to M%1").arg(memoryIdx+1), m_contextMenu); + connect(applyOffsetAction, &QAction::triggered, this, [this, memoryIdx]()->void { + applyOffsetToMem(memoryIdx); + }); + m_contextMenu->addAction(applyOffsetAction); + applyOffsetAction->setEnabled(memNotEmpty); + + // Smooth + QAction* smoothAction = new QAction(QString("Smooth M%1").arg(memoryIdx+1), m_contextMenu); + connect(smoothAction, &QAction::triggered, this, [this, memoryIdx]()->void { + smoothMem(memoryIdx); + }); + m_contextMenu->addAction(smoothAction); + smoothAction->setEnabled(memNotEmpty); + + bool memAAndBNotEmpty = (m_settings.m_spectrumMemory[0].m_spectrum.size() > 0) + && (m_settings.m_spectrumMemory[1].m_spectrum.size() > 0); + + // Add mems + QAction* addMemAction = new QAction(QString("Set M%1 to M1+M2").arg(memoryIdx+1), m_contextMenu); + connect(addMemAction, &QAction::triggered, this, [this, memoryIdx]()->void { + addMem(memoryIdx); + }); + m_contextMenu->addAction(addMemAction); + addMemAction->setEnabled(memAAndBNotEmpty); + + // Diff mems + QAction* diffMemAction = new QAction(QString("Set M%1 to M1-M2").arg(memoryIdx+1), m_contextMenu); + connect(diffMemAction, &QAction::triggered, this, [this, memoryIdx]()->void { + diffMem(memoryIdx); + }); + m_contextMenu->addAction(diffMemAction); + diffMemAction->setEnabled(memAAndBNotEmpty); + + m_contextMenu->addSeparator(); + + // Load memory from .csv + QAction* loadSpectrumAction = new QAction(QString("Load M%1 from .csv").arg(memoryIdx+1), m_contextMenu); + connect(loadSpectrumAction, &QAction::triggered, this, [this, memoryIdx]()->void { + loadMem(memoryIdx); + }); + m_contextMenu->addAction(loadSpectrumAction); + + // Save memory to .csv + QAction* saveSpectrumAction = new QAction(QString("Save M%1 to .csv").arg(memoryIdx+1), m_contextMenu); + connect(saveSpectrumAction, &QAction::triggered, this, [this, memoryIdx]()->void { + saveMem(memoryIdx); + }); + m_contextMenu->addAction(saveSpectrumAction); + saveSpectrumAction->setEnabled(memNotEmpty); + + m_contextMenu->addSeparator(); + + QAction* infoAction = new QAction(QString("M%1 has %2 elements").arg(memoryIdx+1).arg(m_settings.m_spectrumMemory[memoryIdx].m_spectrum.size()), m_contextMenu); + m_contextMenu->addAction(infoAction); + infoAction->setEnabled(false); + + m_contextMenu->popup(p); + } +} + +// Convert spectrum from dB to linear or vice-versa +void GLSpectrumGUI::convertSpectrum(QList &spectrum, bool fromDB, bool toDB) const +{ + if (toDB && !fromDB) + { + for (int i = 0; i < spectrum.size(); i++) { + spectrum[i] = CalcDb::dbPower(spectrum[i]); + } + } + else if (!toDB && fromDB) + { + for (int i = 0; i < spectrum.size(); i++) { + spectrum[i] = CalcDb::powerFromdB(spectrum[i]); + } + } +} + +// Set math memory in SpectrumVis to selected memory +void GLSpectrumGUI::setMathMemory() +{ + if (m_spectrumVis) + { + QList spectrum; + + if ( (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM1) + || (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM1DB) + || (m_settings.m_mathMode == SpectrumSettings::MathModeAbsXMinusM1DB) + ) + { + spectrum = m_settings.m_spectrumMemory[0].m_spectrum; + } else if ( (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM2) + || (m_settings.m_mathMode == SpectrumSettings::MathModeXMinusM2DB) + || (m_settings.m_mathMode == SpectrumSettings::MathModeAbsXMinusM2DB) + ) + { + spectrum = m_settings.m_spectrumMemory[1].m_spectrum; + } + else + { + return; + } + + convertSpectrum(spectrum, !m_settings.m_linear, m_settings.mathUsesDB()); + m_spectrumVis->setMathMemory(spectrum); + } +} + +// Load from .csv file in to specified memory +void GLSpectrumGUI::loadMem(int memoryIdx) +{ + QFileDialog fileDialog(nullptr, "Select file to load data from", "", "*.csv"); + fileDialog.setDefaultSuffix("csv"); + fileDialog.setAcceptMode(QFileDialog::AcceptOpen); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + // Read from csv file + QFile file(fileNames[0]); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QTextStream in(&file); + QString error; + QHash colIndexes = CSV::readHeader(in, {"Power"}, error); + QList spectrum; + + if (error.isEmpty()) + { + int powerCol = colIndexes.value("Power"); + int maxCol = std::max({powerCol}); + QStringList cols; + + while (CSV::readRow(in, &cols) && error.isEmpty()) + { + if (cols.size() > maxCol) + { + bool ok; + float value = cols[powerCol].toFloat(&ok); + if (ok) { + spectrum.push_back(value); + } else { + error = QString("Failed to convert %1 to float").arg(cols[powerCol]); + } + } + } + } + if (error.isEmpty()) + { + m_settings.m_spectrumMemory[memoryIdx].m_spectrum = spectrum; + updateMem(memoryIdx); + + if (spectrum.size() != m_settings.m_fftSize) { + QMessageBox::warning(this, "Spectrum", QString("Loaded data size (%1) does not match FFT size (%2)").arg(spectrum.size()).arg(m_settings.m_fftSize)); + } + } + else + { + QMessageBox::critical(this, "Spectrum", QString("Failed to read file %1\n%2").arg(fileNames[0]).arg(error)); + } + } + else + { + QMessageBox::critical(this, "Spectrum", QString("Failed to open file %1").arg(fileNames[0])); + } + } + } +} + +// Save from specified memory to .csv file +void GLSpectrumGUI::saveMem(int memoryIdx) +{ + // Get filename to write + QFileDialog fileDialog(nullptr, "Select file to save data to", "", "*.csv"); + + fileDialog.setDefaultSuffix("csv"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + + if (fileNames.size() > 0) + { + // Write to csv file + QFile file(fileNames[0]); + + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) + { + QTextStream out(&file); + + out << "\"Power\"\n"; + for (int i = 0; i < m_settings.m_spectrumMemory[memoryIdx].m_spectrum.size(); i++) { + out << m_settings.m_spectrumMemory[memoryIdx].m_spectrum[i] << "\n"; + } + + file.close(); + } + else + { + QMessageBox::critical(this, "Spectrum", QString("Failed to open file %1").arg(fileNames[0])); + } + } + } +} + +void GLSpectrumGUI::applyOffsetToMem(int memoryIdx) +{ + double offset = QInputDialog::getDouble(this, "Enter offset to apply", "Offset"); + + for (int i = 0; i < m_settings.m_spectrumMemory[memoryIdx].m_spectrum.size(); i++) { + m_settings.m_spectrumMemory[memoryIdx].m_spectrum[i] += offset; + } + + updateMem(memoryIdx); +} + +void GLSpectrumGUI::smoothMem(int memoryIdx) +{ + int n = 5; + int l = n / 2; + std::size_t size1 = m_settings.m_spectrumMemory[memoryIdx].m_spectrum.size(); + std::size_t size2 = size1 + (n - 1); + Real *temp = new Real[size2]; + + // Copy and replicate sample at each end + for (int i = 0; i < l; i++) + { + temp[i] = m_settings.m_spectrumMemory[memoryIdx].m_spectrum[0]; + temp[size2-1-i] = m_settings.m_spectrumMemory[memoryIdx].m_spectrum[size1 - 1]; + } + std::copy(m_settings.m_spectrumMemory[memoryIdx].m_spectrum.begin(), m_settings.m_spectrumMemory[memoryIdx].m_spectrum.end(), &temp[l]); + + // Average n consequtive samples + for (std::size_t i = 0; i < size1; i++) + { + Real sum = 0.0; + for (int j = 0; j < n; j++) { + sum += temp[i+j]; + } + Real avg = sum / (Real) n; + m_settings.m_spectrumMemory[memoryIdx].m_spectrum[i] = avg; + } + + delete[] temp; + + updateMem(memoryIdx); +} + +void GLSpectrumGUI::addMem(int memoryIdx) +{ + std::size_t s = std::min(m_settings.m_spectrumMemory[0].m_spectrum.size(), m_settings.m_spectrumMemory[1].m_spectrum.size()); + + for (std::size_t i = 0; i < s; i++) + { + m_settings.m_spectrumMemory[memoryIdx].m_spectrum[i] = + m_settings.m_spectrumMemory[0].m_spectrum[i] + + m_settings.m_spectrumMemory[1].m_spectrum[i]; + } + + updateMem(memoryIdx); +} + +void GLSpectrumGUI::diffMem(int memoryIdx) +{ + std::size_t s = std::min(m_settings.m_spectrumMemory[0].m_spectrum.size(), m_settings.m_spectrumMemory[1].m_spectrum.size()); + + for (std::size_t i = 0; i < s; i++) + { + m_settings.m_spectrumMemory[memoryIdx].m_spectrum[i] = + m_settings.m_spectrumMemory[0].m_spectrum[i] + - m_settings.m_spectrumMemory[1].m_spectrum[i]; + } + + updateMem(memoryIdx); +} + +void GLSpectrumGUI::updateMem(int memoryIdx) +{ + // Update in GLSpectrum + m_glSpectrum->setMemory(memoryIdx, m_settings.m_spectrumMemory[memoryIdx]); + // Update in SpectrumVIS + setMathMemory(); +} + +// Look through items of numerical values using engineering units in a combo box, looking for text with equivalent value. E.g. 1000 == 1k +int GLSpectrumGUI::findEquivalentText(QComboBox *combo, int value) +{ + for (int i = 0; i < combo->count(); i++) + { + int itemValue = Units::engUnitsToPosInt(combo->itemText(i)); + if (itemValue == value) { + return i; + } + } + return -1; +} diff --git a/sdrgui/gui/glspectrumgui.h b/sdrgui/gui/glspectrumgui.h index 0cdc2562e..960e2f9a8 100644 --- a/sdrgui/gui/glspectrumgui.h +++ b/sdrgui/gui/glspectrumgui.h @@ -26,6 +26,8 @@ #define INCLUDE_GLSPECTRUMGUI_H #include +#include +#include #include "dsp/dsptypes.h" #include "dsp/spectrumsettings.h" @@ -45,13 +47,6 @@ class SDRGUI_API GLSpectrumGUI : public QWidget, public Serializable { Q_OBJECT public: - enum AveragingMode - { - AvgModeNone, - AvgModeMoving, - AvgModeFixed, - AvgModeMax - }; explicit GLSpectrumGUI(QWidget* parent = NULL); ~GLSpectrumGUI(); @@ -77,10 +72,12 @@ private: Real m_calibrationShiftdB; static const int m_fpsMs[]; SpectrumMarkersDialog *m_markersDialog; + QMenu *m_contextMenu; void blockApplySettings(bool block); void applySettings(); void applySpectrumSettings(); + void setPowerAndRefRange(); void displaySettings(); void displayControls(); void setAveragingCombo(); @@ -92,6 +89,17 @@ private: bool handleMessage(const Message& message); void displayGotoMarkers(); QString displayScaled(int64_t value, char type, int precision, bool showMult); + void setMathMemory(); + void convertSpectrum(QList &spectrum, bool fromDB, bool toDB) const; + void memContextMenu(int memoryIdx, const QPoint &p); + void loadMem(int memoryIdx); + void saveMem(int memoryIdx); + void updateMem(int memoryIdx); + void applyOffsetToMem(int memoryIdx); + void smoothMem(int memoryIdx); + void addMem(int memoryIdx); + void diffMem(int memoryIdx); + int findEquivalentText(QComboBox *combo, int value); private slots: void on_fftWindow_currentIndexChanged(int index); @@ -111,10 +119,15 @@ private slots: void on_traceIntensity_valueChanged(int index); void on_averagingMode_currentIndexChanged(int index); void on_averaging_currentIndexChanged(int index); - void on_linscale_toggled(bool checked); + void on_mathMode_currentIndexChanged(int index); + void on_mathAvgCount_currentIndexChanged(int index); + void on_mathAvgCount_editingFinished(); + void on_linscale_toggled(bool checked); void on_wsSpectrum_toggled(bool checked); void on_markers_clicked(bool checked); + void on_load_clicked(bool checked); void on_save_clicked(bool checked); + void on_saveImage_clicked(bool checked); void on_waterfall_toggled(bool checked); void on_spectrogram_toggled(bool checked); @@ -129,7 +142,7 @@ private slots: void on_freeze_toggled(bool checked); void on_calibration_toggled(bool checked); void on_gotoMarker_currentIndexChanged(int index); - void on_showAllControls_toggled(bool checked); + void on_showControls_currentIndexChanged(int index); void on_measure_clicked(bool checked); void on_resetMeasurements_clicked(bool checked); @@ -146,6 +159,12 @@ private slots: void updateMeasurements(); void closeMarkersDialog(); + void on_showSettingsDialog_clicked(bool checked=false); + void on_mem1_clicked(bool checked=false); + void on_mem2_clicked(bool checked=false); + void mem1ContextMenu(const QPoint &p); + void mem2ContextMenu(const QPoint &p); + signals: // Emitted when user selects an annotation marker void requestCenterFrequency(qint64 frequency); diff --git a/sdrgui/gui/glspectrumgui.ui b/sdrgui/gui/glspectrumgui.ui index a5f561de1..d6b616347 100644 --- a/sdrgui/gui/glspectrumgui.ui +++ b/sdrgui/gui/glspectrumgui.ui @@ -112,7 +112,7 @@ - + 0 @@ -533,7 +533,7 @@ FFT window function - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -606,7 +606,7 @@ FFT size - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -665,14 +665,20 @@ 60 - 0 + 24 + + + + + 100 + 16777215 FFT overlap - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 0 @@ -719,6 +725,11 @@ Max + + + Min + + @@ -835,6 +846,169 @@ + + + + + 88 + 16777215 + + + + Math function to apply to spectrum data + + + 0 + + + + 16 + 16 + + + + + No + + + + + x-μ + + + + + x-μ dB + + + + + x-μ+∧μ dB + + + + + |x-μ| dB + + + + + x-M1 + + + + + x-M1 dB + + + + + |x-M1| dB + + + + + x-M2 + + + + + x-M2 dB + + + + + |x-M2| dB + + + + + + + + + 55 + 16777215 + + + + Number of samples in moving average. [2,1M] + + + true + + + + 2 + + + + + 5 + + + + + 10 + + + + + 20 + + + + + 50 + + + + + 100 + + + + + 200 + + + + + 500 + + + + + 1k + + + + + 2k + + + + + 5k + + + + + 10k + + + + + 20k + + + + + 50k + + + + @@ -895,13 +1069,13 @@ Reference level (dB) - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - -200 + -120 - 40 + 0 @@ -923,13 +1097,13 @@ Range (dB) - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 1 - 100 + 120 @@ -1031,6 +1205,90 @@ + + + + + 0 + 24 + + + + Left: Toggle display of memory 1 - Right: Show menu + + + M1 + + + true + + + + + + + + 0 + 24 + + + + Left: Toggle display of memory 2 - Right: Show menu + + + M2 + + + true + + + + + + + Load spectrum data from .csv file + + + + + + + :/load.png:/load.png + + + + + + + Save spectrum data to .csv file + + + + + + + :/save.png:/save.png + + + + + + + Save display to image file (*.png / *.jpg) + + + + + + + :/picture.png:/picture.png + + + + + + + @@ -1046,20 +1304,6 @@ - - - - Save spectrum data to file - - - - - - - :/save.png:/save.png - - - @@ -1076,7 +1320,7 @@ - + 0 @@ -1097,7 +1341,7 @@ - + 0 @@ -1145,26 +1389,51 @@ - + - Toggle all controls + Show settings dialog - Grid + :/listing.png:/listing.png - + + + + + - 16 - 16 + 0 + 0 - - true + + + 52 + 16777215 + + + Which controls to show (Minimum / Standard / All) + + + + Min + + + + + Std + + + + + All + + diff --git a/sdrgui/gui/glspectrumview.cpp b/sdrgui/gui/glspectrumview.cpp index deff1101d..e21917fcb 100644 --- a/sdrgui/gui/glspectrumview.cpp +++ b/sdrgui/gui/glspectrumview.cpp @@ -25,6 +25,7 @@ #include +#include #include #include #include @@ -42,6 +43,7 @@ #include "util/messagequeue.h" #include "util/db.h" #include "util/profiler.h" +#include "util/csv.h" #include @@ -52,6 +54,7 @@ MESSAGE_CLASS_DEFINITION(GLSpectrumView::MsgReportPowerScale, Message) MESSAGE_CLASS_DEFINITION(GLSpectrumView::MsgReportCalibrationShift, Message) MESSAGE_CLASS_DEFINITION(GLSpectrumView::MsgReportHistogramMarkersChange, Message) MESSAGE_CLASS_DEFINITION(GLSpectrumView::MsgReportWaterfallMarkersChange, Message) +MESSAGE_CLASS_DEFINITION(GLSpectrumView::MsgFrequencyZooming, Message) const float GLSpectrumView::m_maxFrequencyZoom = 50.0f; const float GLSpectrumView::m_annotationMarkerHeight = 20.0f; @@ -66,9 +69,14 @@ GLSpectrumView::GLSpectrumView(QWidget* parent) : m_fpsPeriodMs(50), m_mouseInside(false), m_changesPending(true), + m_redrawAll(false), m_centerFrequency(100000000), - m_referenceLevel(0), - m_powerRange(100), + m_referenceLevel(0.0f), + m_minReferenceLevel(-120.0f), + m_maxReferenceLevel(0.0f), + m_powerRange(100.0), + m_minPowerRange(1.0f), + m_maxPowerRange(120.0f), m_linear(false), m_decay(1), m_sampleRate(500000), @@ -76,6 +84,8 @@ GLSpectrumView::GLSpectrumView(QWidget* parent) : m_fftOverlap(0), m_fftSize(512), m_nbBins(512), + m_fftMin(0), + m_fftMax(512), m_displayGrid(true), m_displayGridIntensity(5), m_displayTraceIntensity(50), @@ -106,6 +116,7 @@ GLSpectrumView::GLSpectrumView(QWidget* parent) : m_pan3DSpectrogram(false), m_scaleZ3DSpectrogram(false), m_3DSpectrogramStyle(SpectrumSettings::Outline), + m_spectrumColor(255, 255, 63), m_colorMapName("Angel"), m_scrollFrequency(false), m_scrollStartCenterFreq(0), @@ -137,7 +148,25 @@ GLSpectrumView::GLSpectrumView(QWidget* parent) : m_measurementHarmonics(5), m_measurementPeaks(5), m_measurementHighlight(true), - m_measurementPrecision(1) + m_measurementPrecision(1), + m_measurementMemMasks(0), + m_maskTestCount(SpectrumSettings::m_maxSpectrumMemories), + m_maskFailCount(SpectrumSettings::m_maxSpectrumMemories), + m_maskFails(SpectrumSettings::m_maxSpectrumMemories), + m_displayRBW(false), + m_displayCursorStats(false), + m_displayPeakStats(false), + m_cursorOverSpectrum(false), + m_cursorFrequency(0.0f), + m_spectrumBuffer(0), + m_spectrumBufferFFTSize(0), + m_spectrumBufferMaxSize(100000), + m_scrollBar(nullptr), + m_scrollBarEnabled(false), + m_scrollBarValue(0), + m_waterfallTimeUnits(SpectrumSettings::TimeOffset), + m_waterfallTimeFormat("hh:mm:ss"), + m_spectrumMemory(SpectrumSettings::m_maxSpectrumMemories) { // Enable multisampling anti-aliasing (MSAA) int multisamples = MainCore::instance()->getSettings().getMultisampling(); @@ -275,6 +304,8 @@ GLSpectrumView::~GLSpectrumView() delete m_openGLLogger; m_openGLLogger = nullptr; } + + clearSpectrumBuffer(); } void GLSpectrumView::queueRequestCenterFrequency(qint64 frequency) @@ -322,6 +353,7 @@ void GLSpectrumView::setReferenceLevel(Real referenceLevel) m_mutex.lock(); m_referenceLevel = referenceLevel; m_changesPending = true; + redrawWaterfallAnd3DSpectrogram(); m_mutex.unlock(); update(); } @@ -331,6 +363,7 @@ void GLSpectrumView::setPowerRange(Real powerRange) m_mutex.lock(); m_powerRange = powerRange; m_changesPending = true; + redrawWaterfallAnd3DSpectrogram(); m_mutex.unlock(); update(); } @@ -421,11 +454,22 @@ void GLSpectrumView::set3DSpectrogramStyle(SpectrumSettings::SpectrogramStyle st update(); } +void GLSpectrumView::setSpectrumColor(QRgb color) +{ + m_mutex.lock(); + m_spectrumColor = QColor(color); + m_changesPending = true; + m_redrawAll = true; + m_mutex.unlock(); + update(); +} + void GLSpectrumView::setColorMapName(const QString &colorMapName) { m_mutex.lock(); m_colorMapName = colorMapName; m_changesPending = true; + m_redrawAll = true; m_mutex.unlock(); update(); } @@ -448,6 +492,9 @@ void GLSpectrumView::setInvertedWaterfall(bool inv) m_invertedWaterfall = inv; m_changesPending = true; stopDrag(); + if (m_scrollBar) { + m_scrollBar->setInvertedAppearance(!inv); + } m_mutex.unlock(); update(); } @@ -573,7 +620,8 @@ void GLSpectrumView::setMeasurementParams( int harmonics, int peaks, bool highlight, - int precision + int precision, + unsigned memoryMask ) { m_mutex.lock(); @@ -586,14 +634,30 @@ void GLSpectrumView::setMeasurementParams( m_measurementPeaks = peaks; m_measurementHighlight = highlight; m_measurementPrecision = precision; + m_measurementMemMasks = memoryMask; m_changesPending = true; if (m_measurements) { - m_measurements->setMeasurementParams(measurement, peaks, precision); + m_measurements->setMeasurementParams(measurement, peaks, precision, memoryMask); } m_mutex.unlock(); update(); } +void GLSpectrumView::resetMeasurements() +{ + m_mutex.lock(); + for (std::size_t i = 0; i < m_maskFails.size(); ++i) + { + m_maskTestCount[i] = 0; + m_maskFailCount[i] = 0; + int s = std::min((int) m_maskFails[i].size(), (int) m_spectrumMemory[i].m_spectrum.size()); + for (int j = 0; j < s; j++) { + m_maskFails[i][j] = m_spectrumMemory[i].m_spectrum[j]; + } + } + m_mutex.unlock(); +} + void GLSpectrumView::addChannelMarker(ChannelMarker* channelMarker) { m_mutex.lock(); @@ -697,99 +761,487 @@ float GLSpectrumView::getTimeMax() const return m_timeScale.getRangeMax(); } -void GLSpectrumView::newSpectrum(const Real *spectrum, int nbBins, int fftSize) +void GLSpectrumView::setWaterfallTimeFormat(SpectrumSettings::WaterfallTimeUnits waterfallTimeUnits, const QString& format) { QMutexLocker mutexLocker(&m_mutex); - m_displayChanged = true; - if (m_changesPending) - { - m_fftSize = fftSize; - m_nbBins = nbBins; - return; + m_waterfallTimeUnits = waterfallTimeUnits; + m_waterfallTimeFormat = format; + if (m_waterfallTimeUnits == SpectrumSettings::TimeOffset) { + m_timeScale.setTickFormatter(nullptr); + } else { + m_timeScale.setTickFormatter(this); } - - if ((fftSize != m_fftSize) || (m_nbBins != nbBins)) - { - m_fftSize = fftSize; - m_nbBins = nbBins; - m_changesPending = true; - return; - } - - updateWaterfall(spectrum); - update3DSpectrogram(spectrum); - updateHistogram(spectrum); + m_changesPending = true; } -void GLSpectrumView::updateWaterfall(const Real *spectrum) +void GLSpectrumView::setStatusLine(bool displayRBW, bool displayCursorStats, bool displayPeakStats) +{ + m_displayRBW = displayRBW; + m_displayCursorStats = displayCursorStats; + m_displayPeakStats = displayPeakStats; + m_displayChanged = true; +} + +void GLSpectrumView::setScrolling(bool enabled, int length) +{ + m_scrollBar->setVisible(enabled); // Must call before we lock mutex, otherwise we can deadlock + + QMutexLocker mutexLocker(&m_mutex); + + if (!enabled) + { + if (!m_spectrumBuffer.isEmpty()) { + clearSpectrumBuffer(); + } + } + else + { + while (m_spectrumBuffer.size() > (std::size_t) length) + { + if (m_currentSpectrum == m_spectrumBuffer.takeFirst().m_spectrum) { + m_currentSpectrum = nullptr; + } + delete[] m_spectrumBuffer.takeFirst().m_spectrum; + } + } + m_spectrumBuffer.resize(length); + m_spectrumBufferMaxSize = length; + m_scrollBarEnabled = enabled; +} + +void GLSpectrumView::setScrollBar(QScrollBar* scrollBar) +{ + m_scrollBar = scrollBar; + connect(m_scrollBar, &QScrollBar::valueChanged, this, &GLSpectrumView::scrollBarValueChanged); +} + +void GLSpectrumView::updateScrollBar() +{ + QMutexLocker mutexLocker(&m_mutex); + + if (m_scrollBar && m_waterfallBuffer) + { + qint64 max = std::max(0, (m_spectrumBuffer.size() - 1 - m_waterfallBuffer->height())); + + m_scrollBar->setMaximum(max); + m_scrollBar->setPageStep(m_waterfallBuffer->height()); + } +} + +void GLSpectrumView::scrollBarValueChanged(int value) +{ + m_scrollBarValue = value; + + m_redrawAll = true; + m_changesPending = true; // Update waterfall time scale + m_displayChanged = true; +} + +int GLSpectrumView::scrollBarValue() const +{ + // We keep a local copy of scroll bar value, so it can be accessed by any thread + return m_scrollBarValue; +} + +// Read spectrum from .csv file +void GLSpectrumView::readCSV(QTextStream &in, bool append, QString &error) +{ + QMutexLocker mutexLocker(&m_mutex); + + if (!append) + { + clearSpectrumBuffer(); + m_redrawAll = true; + m_changesPending = true; + } + + QHash colIndexes = CSV::readHeader(in, {}, error); + + if (error.isEmpty()) + { + if (colIndexes.contains("Frequency") && colIndexes.contains("Power")) + { + int frequencyCol = colIndexes.value("Frequency"); + int powerCol = colIndexes.value("Power"); + int maxCol = std::max({frequencyCol, powerCol}); + QStringList cols; + QVector frequencies; + QVector power; + + while (CSV::readRow(in, &cols)) + { + if (cols.size() > maxCol) + { + frequencies.append(cols[frequencyCol].toLongLong()); + power.append(cols[powerCol].toFloat()); + } + } + if (power.size() != m_fftSize) { + error = QString("CSV data does not contain expected number of points for current FFT size. (Points: %1 - FFT size: %2").arg(power.size()).arg(m_fftSize); + } else { + newSpectrum(power.data(), m_fftSize, m_sampleRate, frequencies[m_fftSize/2], QDateTime::currentDateTime()); + } + } + else if (colIndexes.contains("Date and Time") && colIndexes.contains("Center Frequency (Hz)") && colIndexes.contains("Sample Rate (Hz)") && colIndexes.contains("Power")) + { + int dateTimeCol = colIndexes.value("Date and Time"); + int frequencyCol = colIndexes.value("Center Frequency (Hz)"); + int sampleRateCol = colIndexes.value("Sample Rate (Hz)"); + int powerCol = colIndexes.value("Power"); + int maxCol = std::max({dateTimeCol, frequencyCol, sampleRateCol, powerCol}); + QStringList cols; + QVector spectrum; + + while (CSV::readRow(in, &cols)) + { + if (cols.size() > maxCol) + { + QDateTime dateTime = QDateTime::fromString(cols[dateTimeCol], Qt::ISODateWithMs); + qint64 centerFrequency = cols[frequencyCol].toLongLong(); + quint32 sampleRate = cols[sampleRateCol].toUInt(); + + spectrum.clear(); + for (int i = 0; i < m_fftSize; i++) + { + if (powerCol + i < cols.size()) { + spectrum.append(cols[powerCol + i].toFloat()); + } + } + if (spectrum.size() != m_fftSize) + { + error = QString("CSV data does not contain expected number of points for current FFT size. (Points: %1 - FFT size: %2").arg(spectrum.size()).arg(m_fftSize); + break; + } + else + { + newSpectrum(spectrum.data(), m_fftSize, sampleRate, centerFrequency, dateTime); + } + } + } + } + else + { + error = "CSV header does not contain required columns"; + } + } + + update(); +} + +// Write spectrum to .csv file +void GLSpectrumView::writeCSV(QTextStream &out) +{ + QMutexLocker mutexLocker(&m_mutex); + + if (m_spectrumBuffer.isEmpty()) + { + float frequency = getCenterFrequency() - (getSampleRate() / 2.0f); + float rbw = getSampleRate() / (float) m_fftSize; + out << "\"Frequency\",\"Power\"\n"; + for (int i = 0; i < m_fftSize; i++) + { + out << frequency << "," << m_currentSpectrum[i] << "\n"; + frequency += rbw; + } + } + else + { + out << "\"Date and Time\",\"Center Frequency (Hz)\",\"Sample Rate (Hz)\",\"Power\"\n"; + for (std::size_t j = 0; j < m_spectrumBuffer.size(); j++) + { + out << m_spectrumBuffer[j].m_dateTime.toString(Qt::ISODateWithMs) << "," << m_spectrumBuffer[j].m_centerFrequency << "," << m_spectrumBuffer[j].m_sampleRate << ","; + for (int i = 0; i < m_spectrumBufferFFTSize; i++) { + out << m_spectrumBuffer[j].m_spectrum[i] << ","; + } + out << "\n"; + } + } +} + +// Write spectrum/waterfall image to file +bool GLSpectrumView::writeImage(const QString& filename) +{ + QImage image = grabFramebuffer(); + + return image.save(filename); +} + +// Get center frequency for currently displayed spectrum (which is selected via the scroll bar) +qint64 GLSpectrumView::getDisplayedCenterFrequency() const +{ + int idx = m_spectrumBuffer.size() - 1 - scrollBarValue(); + + if ((idx >= 0) && (idx < (int) m_spectrumBuffer.size())) { + return m_spectrumBuffer[idx].m_centerFrequency; + } else { + return m_centerFrequency; + } +} + +// Get sample rate for currently displayed spectrum (which is selected via the scroll bar) +quint32 GLSpectrumView::getDisplayedSampleRate() const +{ + int idx = m_spectrumBuffer.size() - 1 - scrollBarValue(); + + if ((idx >= 0) && (idx < (int) m_spectrumBuffer.size())) { + return m_spectrumBuffer[idx].m_sampleRate; + } else { + return m_sampleRate; + } +} + +void GLSpectrumView::redrawSpectrum() +{ + if (m_spectrumBuffer.size() > 0) + { + int idx = m_spectrumBuffer.size() - 1 - scrollBarValue(); + + if (idx >= 0 && idx < (int) m_spectrumBuffer.size()) + { + updateHistogram(m_spectrumBuffer[idx].m_spectrum, m_fftMin, m_nbBins); + m_currentSpectrum = m_spectrumBuffer[idx].m_spectrum; + } + } +} + +void GLSpectrumView::redrawWaterfallAnd3DSpectrogram() +{ + if (m_waterfallBuffer && m_spectrumBuffer.size() > 0) + { + int idx = m_spectrumBuffer.size() - 1 - m_waterfallBuffer->height() - scrollBarValue(); + + m_waterfallBufferPos = 0; + m_waterfallTexturePos = 0; + m_3DSpectrogramBufferPos = 0; + m_3DSpectrogramTexturePos = 0; + + for (int i = 0; i < m_waterfallBuffer->height(); i++) + { + if ((idx >= 0) && (idx < (int) m_spectrumBuffer.size())) + { + updateWaterfall(m_spectrumBuffer[idx].m_spectrum, m_fftSize, m_fftMin, m_nbBins); + update3DSpectrogram(m_spectrumBuffer[idx].m_spectrum, m_fftSize, m_fftMin, m_nbBins); + } + else + { + clearWaterfallRow(m_nbBins); + clear3DSpectrogramRow(m_nbBins); + } + idx++; + } + } +} + +void GLSpectrumView::measure(const Real *spectrum, bool updateGUI) +{ + switch (m_measurement) + { + case SpectrumSettings::MeasurementPeaks: + if (updateGUI) { + measurePeaks(spectrum); + } + break; + case SpectrumSettings::MeasurementChannelPower: + measureChannelPower(spectrum, updateGUI); + break; + case SpectrumSettings::MeasurementAdjacentChannelPower: + measureAdjacentChannelPower(spectrum, updateGUI); + break; + case SpectrumSettings::MeasurementOccupiedBandwidth: + measureOccupiedBandwidth(spectrum, updateGUI); + break; + case SpectrumSettings::Measurement3dBBandwidth: + measure3dBBandwidth(spectrum, updateGUI); + break; + case SpectrumSettings::MeasurementSNR: + measureSNR(spectrum, updateGUI); + measureSFDR(spectrum, updateGUI); + break; + case SpectrumSettings::MeasurementMask: + measureMask(spectrum, m_fftSize, updateGUI); + break; + default: + break; + } +} + +// newSpectrum can be called at a much faster rate than paintGL for high sample rates +// Will typically be called from device source engine thread, so shouldn't touch UI +void GLSpectrumView::newSpectrum(const Real *spectrum, int fftSize) +{ + PROFILER_START(); + + QMutexLocker mutexLocker(&m_mutex); + + newSpectrum(spectrum, fftSize, m_sampleRate, m_centerFrequency, QDateTime::currentDateTime()); + + PROFILER_STOP("newSpectrum"); +} + +void GLSpectrumView::newSpectrum(const Real *spectrum, int fftSize, quint32 sampleRate, qint64 centerFrequency, const QDateTime &dateTime) +{ + int offset = 0; + int idx; + + m_displayChanged = true; + + if (fftSize != m_fftSize) + { + m_fftSize = fftSize; + updateFFTLimits(true); + m_changesPending = true; + } + + if (m_scrollBarEnabled) + { + updateSpectrumBuffer(&spectrum[0], m_fftSize, sampleRate, centerFrequency, dateTime); + offset = scrollBarValue(); + idx = m_spectrumBuffer.size() - 1 - offset; + m_currentSpectrum = m_spectrumBuffer[idx].m_spectrum; + } + else + { + updateSpectrumNoBuffer(&spectrum[0], m_fftSize); + m_currentSpectrum = m_spectrumNoBuffer.data(); + } + + measure(spectrum, false); + + if (m_changesPending) { + return; + } + + if (offset == 0) + { + updateWaterfall(spectrum, m_fftSize, m_fftMin, m_nbBins); + update3DSpectrogram(spectrum, m_fftSize, m_fftMin, m_nbBins); + updateHistogram(spectrum, m_fftMin, m_nbBins); + } + else + { + updateWaterfall(m_spectrumBuffer[idx].m_spectrum, m_fftSize, m_fftMin, m_nbBins); + update3DSpectrogram(m_spectrumBuffer[idx].m_spectrum, m_fftSize, m_fftMin, m_nbBins); + updateHistogram(m_spectrumBuffer[idx].m_spectrum, m_fftMin, m_nbBins); + } +} + +void GLSpectrumView::updateSpectrumNoBuffer(const Real *spectrum, int fftSize) +{ + if (m_spectrumNoBuffer.size() != (std::size_t) fftSize) { + m_spectrumNoBuffer.resize(fftSize); + } + + std::copy(spectrum, spectrum + fftSize, m_spectrumNoBuffer.begin()); +} + +void GLSpectrumView::clearSpectrumBuffer() +{ + m_currentSpectrum = nullptr; // Make sure we aren't pointing in to a buffer we're about to delete + for (const auto& spectrumData : m_spectrumBuffer) { + delete[] spectrumData.m_spectrum; + } + m_spectrumBuffer.clear(); +} + +void GLSpectrumView::updateSpectrumBuffer(const Real *spectrum, int fftSize, quint32 sampleRate, qint64 centerFrequency, const QDateTime &dateTime) +{ + // Clear buffer when FFT size changes + if (fftSize != m_spectrumBufferFFTSize) + { + clearSpectrumBuffer(); + m_spectrumBufferFFTSize = fftSize; + } + + // Reuse old buffer if possible, otherwise allocate new buffer + Real *buffer = nullptr; + if (m_spectrumBuffer.size() >= (std::size_t) m_spectrumBufferMaxSize) { + buffer = m_spectrumBuffer.takeFirst().m_spectrum; + } + if (!buffer) { + buffer = new Real[fftSize]; + } + + // Store copy of spectrum and current parameters in spectrum buffer + std::copy(spectrum, spectrum + fftSize, buffer); + Spectrum spectrumData = {buffer, sampleRate, centerFrequency, dateTime}; + m_spectrumBuffer.append(spectrumData); +} + +void GLSpectrumView::clearWaterfallRow(int nbBins) { if (m_waterfallBufferPos < m_waterfallBuffer->height()) { quint32* pix = (quint32*)m_waterfallBuffer->scanLine(m_waterfallBufferPos); - for (int i = 0; i < m_nbBins; i++) + for (int i = 0; i <= nbBins; i++) { + *pix++ = m_waterfallPalette[0]; + } + m_waterfallBufferPos++; + } +} + +void GLSpectrumView::updateWaterfall(const Real *spectrum, int fftSize, int fftMin, int nbBins) +{ + if (m_waterfallBufferPos < m_waterfallBuffer->height()) + { + quint32* pix = (quint32*)m_waterfallBuffer->scanLine(m_waterfallBufferPos); + + for (int i = 0; i < nbBins; i++) { - int v = (int)((spectrum[i] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0); - - if (v > 239) { - v = 239; - } else if (v < 0) { - v = 0; - } - + int v = (int)((spectrum[fftMin + i] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0); + v = clampWaterfall(v); *pix++ = m_waterfallPalette[(int)v]; } - // Replicate Nyquist sample (spectrum[0]) to end of positive side - int v = (int)((spectrum[0] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0); - if (v > 239) { - v = 239; - } else if (v < 0) { - v = 0; - } + int lastIdx = (fftMin + nbBins) % fftSize; + int v = (int)((spectrum[lastIdx] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0); + v = clampWaterfall(v); *pix++ = m_waterfallPalette[(int)v]; m_waterfallBufferPos++; } } -void GLSpectrumView::update3DSpectrogram(const Real *spectrum) +void GLSpectrumView::clear3DSpectrogramRow(int nbBins) { if (m_3DSpectrogramBufferPos < m_3DSpectrogramBuffer->height()) { quint8* pix = (quint8*)m_3DSpectrogramBuffer->scanLine(m_3DSpectrogramBufferPos); - for (int i = 0; i < m_nbBins; i++) + for (int i = 0; i <= nbBins; i++) { + *pix++ = 0; + } + m_3DSpectrogramBufferPos++; + } +} + +void GLSpectrumView::update3DSpectrogram(const Real *spectrum, int fftSize, int fftMin, int nbBins) +{ + if (m_3DSpectrogramBufferPos < m_3DSpectrogramBuffer->height()) + { + quint8* pix = (quint8*)m_3DSpectrogramBuffer->scanLine(m_3DSpectrogramBufferPos); + + for (int i = 0; i < nbBins; i++) { - int v = (int)((spectrum[i] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0); - - if (v > 255) { - v = 255; - } else if (v < 0) { - v = 0; - } - + int v = (int)((spectrum[fftMin + i] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0); + v = clampPixel(v); *pix++ = v; } - // Replicate Nyquist sample (spectrum[0]) to end of positive side - int v = (int)((spectrum[0] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0); - if (v > 255) { - v = 255; - } else if (v < 0) { - v = 0; - } + int lastIdx = (fftMin + nbBins) % fftSize; + int v = (int)((spectrum[lastIdx] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0); + v = clampPixel(v); *pix++ = v; m_3DSpectrogramBufferPos++; } } -void GLSpectrumView::updateHistogram(const Real *spectrum) +void GLSpectrumView::updateHistogram(const Real *spectrum, int fftMin, int nbBins) { quint8* b = m_histogram; - int fftMulSize = 100 * m_nbBins; + int fftMulSize = 100 * nbBins; if ((m_displayHistogram || m_displayMaxHold) && (m_decay != 0)) { @@ -812,8 +1264,6 @@ void GLSpectrumView::updateHistogram(const Real *spectrum) } } - m_currentSpectrum = spectrum; // Store spectrum for current spectrum line display - #if 0 //def USE_SSE2 if(m_decay >= 0) { // normal const __m128 refl = {m_referenceLevel, m_referenceLevel, m_referenceLevel, m_referenceLevel}; @@ -880,9 +1330,9 @@ void GLSpectrumView::updateHistogram(const Real *spectrum) } } #else - for (int i = 0; i < m_nbBins; i++) + for (int i = 0; i < nbBins; i++) { - int v = (int)((spectrum[i] - m_referenceLevel) * 100.0 / m_powerRange + 100.0); + int v = (int)((spectrum[fftMin + i] - m_referenceLevel) * 100.0 / m_powerRange + 100.0); if ((v >= 0) && (v <= 99)) { @@ -994,10 +1444,20 @@ void GLSpectrumView::paintGL() { PROFILER_START() - if (!m_mutex.tryLock(2)) { + if (!m_mutex.tryLock(3)) { // Give time for newSpectrum to complete return; } + if (!m_changesPending && (m_waterfallTimeUnits != SpectrumSettings::TimeOffset)) + { + // Waterfall timescale can change on every repaint when displaying real time + m_timeScale.requestReCalc(); + paintLeftScales(); + if (!m_changesPending) { + m_glShaderLeftScale.initTexture(m_leftMarginPixmap.toImage()); + } + } + if (m_changesPending) { applyChanges(); @@ -1060,7 +1520,6 @@ void GLSpectrumView::paintGL() 0, m_invertedWaterfall ? 1.0f : 0.0f }; - if (m_waterfallTexturePos + m_waterfallBufferPos < m_waterfallTextureHeight) { m_glShaderWaterfall.subTexture(0, m_waterfallTexturePos, m_nbBins + 1, m_waterfallBufferPos, m_waterfallBuffer->scanLine(0)); @@ -1136,6 +1595,9 @@ void GLSpectrumView::paintGL() } } + // When zoomed, use the next sample off screen. When not, replicate the Nyquist bin + const int lastSampleIdx = (m_fftMin + m_nbBins) % m_fftSize; + // paint histogram if (m_displayHistogram || m_displayMaxHold || m_displayCurrent) { @@ -1393,24 +1855,17 @@ void GLSpectrumView::paintGL() GLfloat *q3 = m_q3ColorMap.m_array; for (int i = 0; i < m_nbBins; i++) { - Real v = m_maxHold[i] - m_referenceLevel; - - if (v > 0) { - v = 0; - } else if (v < -m_powerRange) { - v = -m_powerRange; - } + Real v = clampPower(m_maxHold[i] - m_referenceLevel); q3[4*i] = (GLfloat)i; q3[4*i+1] = -m_powerRange; q3[4*i+2] = (GLfloat)i; q3[4*i+3] = v; } - // Replicate Nyquist sample to end of positive side q3[4*m_nbBins] = (GLfloat) m_nbBins; - q3[4*m_nbBins+1] = q3[1]; + q3[4*m_nbBins+1] = -m_powerRange; q3[4*m_nbBins+2] = (GLfloat) m_nbBins; - q3[4*m_nbBins+3] = q3[3]; + q3[4*m_nbBins+3] = clampPower(m_maxHold[lastSampleIdx] - m_referenceLevel); QVector4D color(0.5f, 0.0f, 0.0f, (float) m_displayTraceIntensity / 100.0f); m_glShaderSimple.drawSurfaceStrip(m_glHistogramSpectrumMatrix, color, q3, 2*(m_nbBins+1)); @@ -1421,26 +1876,100 @@ void GLSpectrumView::paintGL() for (int i = 0; i < m_nbBins; i++) { - Real v = m_maxHold[i] - m_referenceLevel; - - if (v >= 0) { - v = 0; - } else if (v < -m_powerRange) { - v = -m_powerRange; - } + Real v = clampPower(m_maxHold[i] - m_referenceLevel); q3[2*i] = (Real) i; q3[2*i+1] = v; } - // Replicate Nyquist sample to end of positive side q3[2*m_nbBins] = (GLfloat) m_nbBins; - q3[2*m_nbBins+1] = q3[1]; + q3[2*m_nbBins+1] = clampPower(m_maxHold[lastSampleIdx] - m_referenceLevel); QVector4D color(1.0f, 0.0f, 0.0f, (float) m_displayTraceIntensity / 100.0f); m_glShaderSimple.drawPolyline(m_glHistogramSpectrumMatrix, color, q3, m_nbBins+1); } } + // paint mask violations + if ((m_measurement == SpectrumSettings::MeasurementMask) && m_measurementHighlight) + { + for (int m = 0; m < m_spectrumMemory.size(); m++) + { + if ( (m_spectrumMemory[m].m_spectrum.size() == m_fftSize) + && (m_maskFails[m].size() == (std::size_t) m_fftSize) + && ((m_measurementMemMasks & (1 << m)) != 0) + ) + { + GLfloat *q3 = m_q3ColorMap.m_array; + for (int i = 0; i < m_nbBins; i++) + { + Real v1 = clampPower(m_maskFails[m][m_fftMin+i] - m_referenceLevel); + Real v2 = clampPower(m_spectrumMemory[m].m_spectrum[m_fftMin+i] - m_referenceLevel); + + q3[4*i] = (GLfloat)i; + q3[4*i+1] = v2; + q3[4*i+2] = (GLfloat)i; + q3[4*i+3] = v1; + } + q3[4*m_nbBins] = (GLfloat) m_nbBins; + q3[4*m_nbBins+1] = clampPower(m_maskFails[m][lastSampleIdx] - m_referenceLevel); + q3[4*m_nbBins+2] = (GLfloat) m_nbBins; + q3[4*m_nbBins+3] = q3[3]; + + QVector4D color(0.5f, 0.0f, 0.0f, (float) m_displayTraceIntensity / 100.0f); + m_glShaderSimple.drawSurfaceStrip(m_glHistogramSpectrumMatrix, color, q3, 2*(m_nbBins+1)); + } + } + } + + // paint memory spectrum as lines + if (m_displayCurrent) + { + for (const auto& memory : m_spectrumMemory) + { + if (memory.m_display && (memory.m_spectrum.size() == m_fftSize)) + { + QColor colorF = QColor::fromRgba(memory.m_color); + + // Draw label + if (!memory.m_label.isEmpty()) + { + float y = (m_powerScale.getRangeMax() - memory.m_spectrum[m_fftMin]) / m_powerScale.getRange() * m_histogramRect.height(); + float h = m_topMargin / (float) height(); + + if ((y >= m_histogramRect.top()) && (y + h < m_histogramRect.bottom())) + { + drawTextOverlay( + memory.m_label, + colorF, + m_textOverlayFont, + 0.0f, + y, + true, + false, // text above the line + m_histogramRect); + } + } + + GLfloat *q3; + + // Draw line + q3 = m_q3FFT.m_array; + for (int i = 0; i < m_nbBins; i++) + { + Real v = clampPower(memory.m_spectrum[m_fftMin+i] - m_referenceLevel); + q3[2*i] = (Real) i; + q3[2*i+1] = v; + } + q3[2*m_nbBins] = (GLfloat) m_nbBins; + q3[2*m_nbBins+1] = clampPower(memory.m_spectrum[lastSampleIdx] - m_referenceLevel); + + QVector4D color; + color = QVector4D(colorF.redF(), colorF.greenF(), colorF.blueF(), colorF.alphaF()); + m_glShaderSimple.drawPolyline(m_glHistogramSpectrumMatrix, color, q3, m_nbBins+1); + } + } + } + // paint current spectrum line on top of histogram if (m_displayCurrent && m_currentSpectrum) { @@ -1453,26 +1982,20 @@ void GLSpectrumView::paintGL() // Fill under line for (int i = 0; i < m_nbBins; i++) { - Real v = m_currentSpectrum[i] - m_referenceLevel; - - if (v > 0) { - v = 0; - } else if (v < bottom) { - v = bottom; - } + Real v = clampPower(m_currentSpectrum[m_fftMin + i] - m_referenceLevel); q3[4*i] = (GLfloat)i; q3[4*i+1] = bottom; q3[4*i+2] = (GLfloat)i; q3[4*i+3] = v; } - // Replicate Nyquist sample to end of positive side - q3[4*m_nbBins] = (GLfloat) m_nbBins; - q3[4*m_nbBins+1] = q3[1]; - q3[4*m_nbBins+2] = (GLfloat) m_nbBins; - q3[4*m_nbBins+3] = q3[3]; - QVector4D color(1.0f, 1.0f, 0.25f, (float) m_displayTraceIntensity / 100.0f); + q3[4*m_nbBins] = (GLfloat) m_nbBins; + q3[4*m_nbBins+1] = bottom; + q3[4*m_nbBins+2] = (GLfloat) m_nbBins; + q3[4*m_nbBins+3] = clampPower(m_currentSpectrum[lastSampleIdx] - m_referenceLevel); + + QVector4D color(m_spectrumColor.redF(), m_spectrumColor.greenF(), m_spectrumColor.blueF(), (float) m_displayTraceIntensity / 100.0f); if (m_spectrumStyle == SpectrumSettings::Gradient) { m_glShaderColorMap.drawSurfaceStrip(m_glHistogramSpectrumMatrix, q3, 2*(m_nbBins+1), bottom, 0.75f); } else { @@ -1482,37 +2005,31 @@ void GLSpectrumView::paintGL() { if (m_histogramFindPeaks) { - m_peakFinder.init(m_currentSpectrum[0]); + m_peakFinder.init(m_currentSpectrum[m_fftMin]); } // Draw line q3 = m_q3FFT.m_array; for (int i = 0; i < m_nbBins; i++) { - Real v = m_currentSpectrum[i] - m_referenceLevel; - - if (v > 0) { - v = 0; - } else if (v < bottom) { - v = bottom; - } + Real v = clampPower(m_currentSpectrum[m_fftMin + i] - m_referenceLevel); q3[2*i] = (Real) i; q3[2*i+1] = v; if (m_histogramFindPeaks && (i > 0)) { - m_peakFinder.push(m_currentSpectrum[i], i == m_nbBins - 1); + m_peakFinder.push(m_currentSpectrum[m_fftMin + i], i == m_nbBins - 1); } } - // Replicate Nyquist sample to end of positive side + q3[2*m_nbBins] = (GLfloat) m_nbBins; - q3[2*m_nbBins+1] = q3[1]; + q3[2*m_nbBins+1] = clampPower(m_currentSpectrum[lastSampleIdx] - m_referenceLevel); QVector4D color; if (m_spectrumStyle == SpectrumSettings::Gradient) { color = QVector4D(m_colorMap[255*3], m_colorMap[255*3+1], m_colorMap[255*3+2], (float) m_displayTraceIntensity / 100.0f); } else { - color = QVector4D(1.0f, 1.0f, 0.25f, (float) m_displayTraceIntensity / 100.0f); + color = QVector4D(m_spectrumColor.redF(), m_spectrumColor.greenF(), m_spectrumColor.blueF(), (float) m_displayTraceIntensity / 100.0f); } m_glShaderSimple.drawPolyline(m_glHistogramSpectrumMatrix, color, q3, m_nbBins+1); @@ -1815,32 +2332,12 @@ void GLSpectrumView::paintGL() m_glShaderInfo.drawSurface(m_glInfoBoxMatrix, tex1, vtx1, 4); } - if (m_currentSpectrum) - { - switch (m_measurement) - { - case SpectrumSettings::MeasurementPeaks: - measurePeaks(); - break; - case SpectrumSettings::MeasurementChannelPower: - measureChannelPower(); - break; - case SpectrumSettings::MeasurementAdjacentChannelPower: - measureAdjacentChannelPower(); - break; - case SpectrumSettings::MeasurementOccupiedBandwidth: - measureOccupiedBandwidth(); - break; - case SpectrumSettings::Measurement3dBBandwidth: - measure3dBBandwidth(); - break; - case SpectrumSettings::MeasurementSNR: - measureSNR(); - measureSFDR(); - break; - default: - break; - } + if (m_displayCursorStats || m_displayPeakStats) { + paintStatusLineRight(); + } + + if (m_currentSpectrum) { + measure(&m_currentSpectrum[m_fftMin], true); } m_mutex.unlock(); @@ -1948,8 +2445,8 @@ void GLSpectrumView::drawSpectrumMarkers() if (m_histogramMarkers.at(i).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePower) { float power = m_linear ? - m_currentSpectrum[m_histogramMarkers.at(i).m_fftBin] * (m_useCalibration ? m_calibrationGain : 1.0f): - m_currentSpectrum[m_histogramMarkers.at(i).m_fftBin] + (m_useCalibration ? m_calibrationShiftdB : 0.0f); + m_currentSpectrum[m_fftMin + m_histogramMarkers.at(i).m_fftBin] * (m_useCalibration ? m_calibrationGain : 1.0f): + m_currentSpectrum[m_fftMin + m_histogramMarkers.at(i).m_fftBin] + (m_useCalibration ? m_calibrationShiftdB : 0.0f); ypoint.ry() = (m_powerScale.getRangeMax() - power) / m_powerScale.getRange(); ypoint.ry() = ypoint.ry() < 0 ? @@ -1963,7 +2460,7 @@ void GLSpectrumView::drawSpectrumMarkers() } else if (m_histogramMarkers.at(i).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePowerMax) { - float power = m_currentSpectrum[m_histogramMarkers.at(i).m_fftBin]; + float power = m_currentSpectrum[m_fftMin + m_histogramMarkers.at(i).m_fftBin]; if ((m_histogramMarkers.at(i).m_holdReset) || (power > m_histogramMarkers[i].m_powerMax)) { @@ -2027,7 +2524,7 @@ void GLSpectrumView::drawSpectrumMarkers() float power0, poweri; if (m_histogramMarkers.at(0).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePower) { - power0 = m_currentSpectrum[m_histogramMarkers.at(0).m_fftBin]; + power0 = m_currentSpectrum[m_fftMin + m_histogramMarkers.at(0).m_fftBin]; } else if (m_histogramMarkers.at(0).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePowerMax) { power0 = m_histogramMarkers.at(0).m_powerMax; } else { @@ -2035,7 +2532,7 @@ void GLSpectrumView::drawSpectrumMarkers() } if (m_histogramMarkers.at(i).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePower) { - poweri = m_currentSpectrum[m_histogramMarkers.at(i).m_fftBin]; + poweri = m_currentSpectrum[m_fftMin + m_histogramMarkers.at(i).m_fftBin]; } else if (m_histogramMarkers.at(i).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePowerMax) { poweri = m_histogramMarkers.at(i).m_powerMax; } else { @@ -2219,55 +2716,25 @@ void GLSpectrumView::drawAnnotationMarkers() } } -// Find and display peak in info line -void GLSpectrumView::measurePeak() -{ - float power, frequency; - - findPeak(power, frequency); - - drawTextsRight( - { - "Peak: ", - "" - }, - { - displayPower(power, m_linear ? 'e' : 'f', m_linear ? 3 : 1), - displayFull(frequency) - }, - { - m_peakPowerMaxStr, - m_peakFrequencyMaxStr - }, - { - m_peakPowerUnits, - "Hz" - } - ); - if (m_measurements) { - m_measurements->setPeak(0, frequency, power); - } -} - // Find and display peaks -void GLSpectrumView::measurePeaks() +void GLSpectrumView::measurePeaks(const Real *spectrum) { // Copy current spectrum so we can modify it - Real *spectrum = new Real[m_nbBins]; - std::copy(m_currentSpectrum, m_currentSpectrum + m_nbBins, spectrum); + Real *spectrumCopy = new Real[m_nbBins]; + std::copy(spectrum, spectrum + m_nbBins, spectrumCopy); for (int i = 0; i < m_measurementPeaks; i++) { // Find peak - int peakBin = findPeakBin(spectrum); + int peakBin = findPeakBin(spectrumCopy); int left, right; - peakWidth(spectrum, peakBin, left, right, 0, m_nbBins); + peakWidth(spectrumCopy, peakBin, left, right, 0, m_nbBins); left++; right--; float power = m_linear ? - spectrum[peakBin] * (m_useCalibration ? m_calibrationGain : 1.0f) : - spectrum[peakBin] + (m_useCalibration ? m_calibrationShiftdB : 0.0f); + spectrumCopy[peakBin] * (m_useCalibration ? m_calibrationGain : 1.0f) : + spectrumCopy[peakBin] + (m_useCalibration ? m_calibrationShiftdB : 0.0f); int64_t frequency = binToFrequency(peakBin); // Add to table @@ -2293,56 +2760,58 @@ void GLSpectrumView::measurePeaks() // Remove peak from spectrum so not found on next pass for (int j = left; j <= right; j++) { - spectrum[j] = -std::numeric_limits::max(); + spectrumCopy[j] = -std::numeric_limits::max(); } } - delete[] spectrum; + delete[] spectrumCopy; } // Calculate and display channel power -void GLSpectrumView::measureChannelPower() +void GLSpectrumView::measureChannelPower(const Real *spectrum, bool updateGUI) { float power; + qint64 centerFrequency = getDisplayedCenterFrequency(); - power = calcChannelPower(m_centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth); + power = calcChannelPower(spectrum, centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth); if (m_measurements) { - m_measurements->setChannelPower(power); + m_measurements->setChannelPower(power, updateGUI); } - if (m_measurementHighlight) { - drawBandwidthMarkers(m_centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth, m_measurementLightMarkerColor); + if (m_measurementHighlight && updateGUI) { + drawBandwidthMarkers(centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth, m_measurementLightMarkerColor); } } // Calculate and display channel power and adjacent channel power -void GLSpectrumView::measureAdjacentChannelPower() +void GLSpectrumView::measureAdjacentChannelPower(const Real *spectrum, bool updateGUI) { float power, powerLeft, powerRight; + qint64 centerFrequency = getDisplayedCenterFrequency(); - power = calcChannelPower(m_centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth); - powerLeft = calcChannelPower(m_centerFrequency + m_measurementCenterFrequencyOffset - m_measurementChSpacing, m_measurementAdjChBandwidth); - powerRight = calcChannelPower(m_centerFrequency + m_measurementCenterFrequencyOffset + m_measurementChSpacing, m_measurementAdjChBandwidth); + power = calcChannelPower(spectrum, centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth); + powerLeft = calcChannelPower(spectrum, centerFrequency + m_measurementCenterFrequencyOffset - m_measurementChSpacing, m_measurementAdjChBandwidth); + powerRight = calcChannelPower(spectrum, centerFrequency + m_measurementCenterFrequencyOffset + m_measurementChSpacing, m_measurementAdjChBandwidth); float leftDiff = powerLeft - power; float rightDiff = powerRight - power; if (m_measurements) { - m_measurements->setAdjacentChannelPower(powerLeft, leftDiff, power, powerRight, rightDiff); + m_measurements->setAdjacentChannelPower(powerLeft, leftDiff, power, powerRight, rightDiff, updateGUI); } - - if (m_measurementHighlight) + if (m_measurementHighlight && updateGUI) { - drawBandwidthMarkers(m_centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth, m_measurementLightMarkerColor); - drawBandwidthMarkers(m_centerFrequency + m_measurementCenterFrequencyOffset - m_measurementChSpacing, m_measurementAdjChBandwidth, m_measurementDarkMarkerColor); - drawBandwidthMarkers(m_centerFrequency + m_measurementCenterFrequencyOffset + m_measurementChSpacing, m_measurementAdjChBandwidth, m_measurementDarkMarkerColor); + drawBandwidthMarkers(centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth, m_measurementLightMarkerColor); + drawBandwidthMarkers(centerFrequency + m_measurementCenterFrequencyOffset - m_measurementChSpacing, m_measurementAdjChBandwidth, m_measurementDarkMarkerColor); + drawBandwidthMarkers(centerFrequency + m_measurementCenterFrequencyOffset + m_measurementChSpacing, m_measurementAdjChBandwidth, m_measurementDarkMarkerColor); } } // Measure bandwidth that has 99% of power -void GLSpectrumView::measureOccupiedBandwidth() +void GLSpectrumView::measureOccupiedBandwidth(const Real *spectrum, bool updateGUI) { - float hzPerBin = m_sampleRate / (float) m_fftSize; - int start = frequencyToBin(m_centerFrequency + m_measurementCenterFrequencyOffset); + float hzPerBin = getDisplayedSampleRate() / (float) m_fftSize; + qint64 centerFrequency = getDisplayedCenterFrequency(); + int start = frequencyToBin(centerFrequency + m_measurementCenterFrequencyOffset); float totalPower, power = 0.0f; int step = 0; int width = 0; @@ -2350,15 +2819,15 @@ void GLSpectrumView::measureOccupiedBandwidth() float gain = m_useCalibration ? m_calibrationGain : 1.0f; float shift = m_useCalibration ? m_calibrationShiftdB : 0.0f; - totalPower = CalcDb::powerFromdB(calcChannelPower(m_centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth)); + totalPower = CalcDb::powerFromdB(calcChannelPower(spectrum, centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth)); do { if ((idx >= 0) && (idx < m_nbBins)) { if (m_linear) { - power += m_currentSpectrum[idx] * gain; + power += spectrum[idx] * gain; } else { - power += CalcDb::powerFromdB(m_currentSpectrum[idx]) + shift; + power += CalcDb::powerFromdB(spectrum[idx]) + shift; } width++; } @@ -2374,27 +2843,28 @@ void GLSpectrumView::measureOccupiedBandwidth() float occupiedBandwidth = width * hzPerBin; if (m_measurements) { - m_measurements->setOccupiedBandwidth(occupiedBandwidth); + m_measurements->setOccupiedBandwidth(occupiedBandwidth, updateGUI); } - if (m_measurementHighlight) + if (m_measurementHighlight && updateGUI) { - drawBandwidthMarkers(m_centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth, m_measurementDarkMarkerColor); - drawBandwidthMarkers(m_centerFrequency + m_measurementCenterFrequencyOffset, occupiedBandwidth, m_measurementLightMarkerColor); + qint64 centerFrequency = getDisplayedCenterFrequency(); + drawBandwidthMarkers(centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth, m_measurementDarkMarkerColor); + drawBandwidthMarkers(centerFrequency + m_measurementCenterFrequencyOffset, occupiedBandwidth, m_measurementLightMarkerColor); } } // Measure bandwidth -3dB from peak -void GLSpectrumView::measure3dBBandwidth() +void GLSpectrumView::measure3dBBandwidth(const Real *spectrum, bool updateGUI) { // Find max peak and it's power in dB - int peakBin = findPeakBin(m_currentSpectrum); - float peakPower = m_linear ? CalcDb::dbPower(m_currentSpectrum[peakBin]) : m_currentSpectrum[peakBin]; + int peakBin = findPeakBin(spectrum); + float peakPower = m_linear ? CalcDb::dbPower(spectrum[peakBin]) : spectrum[peakBin]; // Search right until 3dB from peak int rightBin = peakBin; for (int i = peakBin + 1; i < m_nbBins; i++) { - float power = m_linear ? CalcDb::dbPower(m_currentSpectrum[i]) : m_currentSpectrum[i]; + float power = m_linear ? CalcDb::dbPower(spectrum[i]) : spectrum[i]; if (peakPower - power > 3.0f) { rightBin = i - 1; @@ -2406,7 +2876,7 @@ void GLSpectrumView::measure3dBBandwidth() int leftBin = peakBin; for (int i = peakBin - 1; i >= 0; i--) { - float power = m_linear ? CalcDb::dbPower(m_currentSpectrum[i]) : m_currentSpectrum[i]; + float power = m_linear ? CalcDb::dbPower(spectrum[i]) : spectrum[i]; if (peakPower - power > 3.0f) { leftBin = i + 1; @@ -2417,15 +2887,15 @@ void GLSpectrumView::measure3dBBandwidth() // Calculate bandwidth int bins = rightBin - leftBin - 1; bins = std::max(1, bins); - float hzPerBin = m_sampleRate / (float) m_fftSize; + float hzPerBin = getDisplayedSampleRate() / (float) m_fftSize; float bandwidth = bins * hzPerBin; int centerBin = leftBin + (rightBin - leftBin) / 2; float centerFrequency = binToFrequency(centerBin); if (m_measurements) { - m_measurements->set3dBBandwidth(bandwidth); + m_measurements->set3dBBandwidth(bandwidth, updateGUI); } - if (m_measurementHighlight) { + if (m_measurementHighlight && updateGUI) { drawBandwidthMarkers(centerFrequency, bandwidth, m_measurementLightMarkerColor); } } @@ -2480,30 +2950,30 @@ float GLSpectrumView::calPower(float power) const int GLSpectrumView::frequencyToBin(int64_t frequency) const { - float rbw = (m_ssbSpectrum ? (m_sampleRate/2) : m_sampleRate) / (float)m_fftSize; + float rbw = (m_ssbSpectrum ? (getDisplayedSampleRate()/2) : getDisplayedSampleRate()) / (float)m_fftSize; return (frequency - m_frequencyScale.getRangeMin()) / rbw; } int64_t GLSpectrumView::binToFrequency(int bin) const { - float rbw = (m_ssbSpectrum ? (m_sampleRate/2) : m_sampleRate) / (float)m_fftSize; + float rbw = (m_ssbSpectrum ? (getDisplayedSampleRate()/2) : getDisplayedSampleRate()) / (float)m_fftSize; return m_frequencyScale.getRangeMin() + bin * rbw; } // Find a peak and measure SNR / THD / SINAD -void GLSpectrumView::measureSNR() +void GLSpectrumView::measureSNR(const Real *spectrum, bool updateGUI) { // Find bin with max peak - that will be our signal - int sig = findPeakBin(m_currentSpectrum); + int sig = findPeakBin(spectrum); int sigLeft, sigRight; - peakWidth(m_currentSpectrum, sig, sigLeft, sigRight, 0, m_nbBins); + peakWidth(spectrum, sig, sigLeft, sigRight, 0, m_nbBins); int sigBins = sigRight - sigLeft - 1; int binsLeft = sig - sigLeft; int binsRight = sigRight - sig; // Highlight the signal float sigFreq = binToFrequency(sig); - if (m_measurementHighlight) { + if (m_measurementHighlight && updateGUI) { drawPeakMarkers(binToFrequency(sigLeft+1), binToFrequency(sigRight-1), m_measurementLightMarkerColor); } @@ -2518,16 +2988,16 @@ void GLSpectrumView::measureSNR() { int hBin = frequencyToBin(hFreq); // Check if peak is an adjacent bin - if (m_currentSpectrum[hBin-1] > m_currentSpectrum[hBin]) { + if (spectrum[hBin-1] > spectrum[hBin]) { hBin--; - } else if (m_currentSpectrum[hBin+1] > m_currentSpectrum[hBin]) { + } else if (spectrum[hBin+1] > spectrum[hBin]) { hBin++; } hFreq = binToFrequency(hBin); int hLeft, hRight; - peakWidth(m_currentSpectrum, hBin, hLeft, hRight, hBin - binsLeft, hBin + binsRight); + peakWidth(spectrum, hBin, hLeft, hRight, hBin - binsLeft, hBin + binsRight); int hBins = hRight - hLeft - 1; - if (m_measurementHighlight) { + if (m_measurementHighlight && updateGUI) { drawPeakMarkers(binToFrequency(hLeft+1), binToFrequency(hRight-1), m_measurementDarkMarkerColor); } hBinsLeft.append(hLeft); @@ -2548,9 +3018,9 @@ void GLSpectrumView::measureSNR() { float power; if (m_linear) { - power = m_currentSpectrum[i] * gain; + power = spectrum[i] * gain; } else { - power = CalcDb::powerFromdB(m_currentSpectrum[i]) + shift; + power = CalcDb::powerFromdB(spectrum[i]) + shift; } // Signal power @@ -2613,16 +3083,16 @@ void GLSpectrumView::measureSNR() // Calculate SINAD - Signal to noise and distotion ratio (Should be -THD+N) float sinad = CalcDb::dbPower((sigPower + harmonicPower + noisePower) / (harmonicPower + noisePower)); - m_measurements->setSNR(snr, snfr, thdDB, thdpn, sinad); + m_measurements->setSNR(snr, snfr, thdDB, thdpn, sinad, updateGUI); } } -void GLSpectrumView::measureSFDR() +void GLSpectrumView::measureSFDR(const Real *spectrum, bool updateGUI) { // Find first peak which is our signal - int peakBin = findPeakBin(m_currentSpectrum); + int peakBin = findPeakBin(spectrum); int peakLeft, peakRight; - peakWidth(m_currentSpectrum, peakBin, peakLeft, peakRight, 0, m_nbBins); + peakWidth(spectrum, peakBin, peakLeft, peakRight, 0, m_nbBins); // Find next largest peak, which is the spur int nextPeakBin = -1; @@ -2631,27 +3101,27 @@ void GLSpectrumView::measureSFDR() { if ((i < peakLeft) || (i > peakRight)) { - if (m_currentSpectrum[i] > nextPeakPower) + if (spectrum[i] > nextPeakPower) { nextPeakBin = i; - nextPeakPower = m_currentSpectrum[i]; + nextPeakPower = spectrum[i]; } } } if (nextPeakBin != -1) { // Calculate SFDR in dB from difference between two peaks - float peakPower = calPower(m_currentSpectrum[peakBin]); - float nextPeakPower = calPower(m_currentSpectrum[nextPeakBin]); + float peakPower = calPower(spectrum[peakBin]); + float nextPeakPower = calPower(spectrum[nextPeakBin]); float peakPowerDB = CalcDb::dbPower(peakPower); float nextPeakPowerDB = CalcDb::dbPower(nextPeakPower); float sfdr = peakPowerDB - nextPeakPowerDB; // Display if (m_measurements) { - m_measurements->setSFDR(sfdr); + m_measurements->setSFDR(sfdr, updateGUI); } - if (m_measurementHighlight) + if (m_measurementHighlight && updateGUI) { if (m_linear) { drawPowerBandMarkers(peakPower, nextPeakPower, m_measurementDarkMarkerColor); @@ -2663,17 +3133,17 @@ void GLSpectrumView::measureSFDR() } // Find power and frequency of max peak in current spectrum -void GLSpectrumView::findPeak(float &power, float &frequency) const +void GLSpectrumView::findPeak(const Real *spectrum, float &power, float &frequency) const { int bin; bin = 0; - power = m_currentSpectrum[0]; + power = spectrum[0]; for (int i = 1; i < m_nbBins; i++) { - if (m_currentSpectrum[i] > power) + if (spectrum[i] > power) { - power = m_currentSpectrum[i]; + power = spectrum[i]; bin = i; } } @@ -2685,9 +3155,9 @@ void GLSpectrumView::findPeak(float &power, float &frequency) const } // Calculate channel power in dB -float GLSpectrumView::calcChannelPower(int64_t centerFrequency, int channelBandwidth) const +float GLSpectrumView::calcChannelPower(const Real *spectrum, int64_t centerFrequency, int channelBandwidth) const { - float hzPerBin = m_sampleRate / (float) m_fftSize; + float hzPerBin = getDisplayedSampleRate() / (float) m_fftSize; int bins = channelBandwidth / hzPerBin; int start = frequencyToBin(centerFrequency) - (bins / 2); int end = start + bins; @@ -2700,20 +3170,67 @@ float GLSpectrumView::calcChannelPower(int64_t centerFrequency, int channelBandw { float gain = m_useCalibration ? m_calibrationGain : 1.0f; for (int i = start; i < end; i++) { - power += m_currentSpectrum[i] * gain; + power += spectrum[i] * gain; } } else { float shift = m_useCalibration ? m_calibrationShiftdB : 0.0f; for (int i = start; i < end; i++) { - power += CalcDb::powerFromdB(m_currentSpectrum[i]) + shift; + power += CalcDb::powerFromdB(spectrum[i]) + shift; } } return CalcDb::dbPower(power); } +// Test if current spectrum exceeds any of the masks held in spectrum memories +void GLSpectrumView::measureMask(const Real *spectrum, int fftSize, bool updateGUI) +{ + if (!updateGUI) + { + for (int m = 0; m < m_spectrumMemory.size(); m++) + { + if ((m_measurementMemMasks & (1 << m)) != 0) + { + if (m_maskFails[m].size() < (std::size_t) fftSize) { + m_maskFails[m].resize(fftSize); + } + + int s = std::min((int) m_spectrumMemory[m].m_spectrum.size(), fftSize); + bool fail = false; + + for (int i = 0; i < s; i++) + { + if (spectrum[i] > m_spectrumMemory[m].m_spectrum[i]) + { + m_maskFails[m][i] = std::max(m_maskFails[m][i], spectrum[i]); + fail = true; + } + } + + m_maskTestCount[m]++; + if (fail) { + m_maskFailCount[m]++; + } + } + } + } + else + { + for (int m = 0; m < m_spectrumMemory.size(); m++) + { + if ((m_measurementMemMasks & (1 << m)) != 0) + { + if (m_measurements && updateGUI) { + m_measurements->setMaskTestResult(m, m_maskTestCount[m], m_maskFailCount[m]); + } + } + } + + } +} + void GLSpectrumView::stopDrag() { if (m_cursorState != CSNormal) @@ -2727,28 +3244,137 @@ void GLSpectrumView::stopDrag() } } +// Get text to display on waterfall vertical axis when displaying system time +// value is [0,m_waterfallHeight], as set in setTimeScaleRange() +QString GLSpectrumView::formatTick(double value) const +{ + int idx = value - scrollBarValue() + m_spectrumBuffer.size() - 1 - m_waterfallHeight; + + if ((idx >= 0) && (idx < (int) m_spectrumBuffer.size())) + { + QDateTime dt = m_spectrumBuffer[idx].m_dateTime; + + if (m_waterfallTimeUnits == SpectrumSettings::LocalTime) { + dt = dt.toLocalTime(); + } else { + dt = dt.toUTC(); + } + return dt.toString(m_waterfallTimeFormat); + } + else + { + return ""; + } +} + +void GLSpectrumView::setTimeScaleRange() +{ + if (m_waterfallTimeUnits != SpectrumSettings::TimeOffset) + { + // System clock times from when spectrum was captured - mapped to actual times in formatTick() + if (m_invertedWaterfall) { + m_timeScale.setRange(Unit::None, m_waterfallHeight, 0); + } else { + m_timeScale.setRange(Unit::None, 0, m_waterfallHeight); + } + } + else if (getDisplayedSampleRate() > 0) + { + float timeScaleDiv = ((float)getDisplayedSampleRate() / (float)m_timingRate); + + if (m_fftSize > m_fftOverlap) { + timeScaleDiv *= m_fftSize / (float)(m_fftSize - m_fftOverlap); + } + + int idx = scrollBarValue(); + float timeMin = (idx * m_fftSize) / timeScaleDiv; + float timeMax = ((idx + m_waterfallHeight) * m_fftSize) / timeScaleDiv; + + if (!m_invertedWaterfall) { + m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, timeMax / timeScaleDiv, timeMin); + } else { + m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, timeMin, timeMax); + } + } + else + { + if (!m_invertedWaterfall) { + m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, 0, 1); + } else { + m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, 1, 0); + } + } +} + +void GLSpectrumView::paintLeftScales() +{ + QFontMetrics fm(font()); + int M = fm.horizontalAdvance("-"); + + float maxSize = 0.0f; + + m_leftMarginPixmap = QPixmap(m_leftMargin - 1, height()); + m_leftMarginPixmap.fill(Qt::transparent); + { + QPainter painter(&m_leftMarginPixmap); + painter.setPen(QColor(0xf0, 0xf0, 0xff)); + painter.setFont(font()); + const ScaleEngine::TickList* tickList; + const ScaleEngine::Tick* tick; + if (m_displayWaterfall) { + tickList = &m_timeScale.getTickList(); + for (int i = 0; i < tickList->count(); i++) { + tick = &(*tickList)[i]; + if (tick->major) + { + if (tick->textSize > 0) + painter.drawText(QPointF(m_leftMargin - M - tick->textSize, m_waterfallTop + fm.ascent() + tick->textPos), tick->text); + maxSize = std::max(maxSize, tick->textSize); + } + } + } + if (m_displayHistogram || m_displayMaxHold || m_displayCurrent) { + tickList = &m_powerScale.getTickList(); + for (int i = 0; i < tickList->count(); i++) { + tick = &(*tickList)[i]; + if (tick->major) { + if (tick->textSize > 0) + painter.drawText(QPointF(m_leftMargin - M - tick->textSize, m_histogramTop + m_histogramHeight - tick->textPos - 1), tick->text); + maxSize = std::max(maxSize, tick->textSize); + } + } + } + } + + if (maxSize >= m_leftMargin) { + m_changesPending = true; // Recalculate margin + } +} + void GLSpectrumView::applyChanges() { if (m_nbBins <= 0) { return; } + qint64 centerFrequency = getDisplayedCenterFrequency(); + QFontMetrics fm(font()); int M = fm.horizontalAdvance("-"); - m_topMargin = fm.ascent() * 2.0; - m_bottomMargin = fm.ascent() * 1.0; + m_topMargin = fm.ascent() * 2; + m_bottomMargin = fm.ascent() * 1; m_infoHeight = fm.height() * 3; - int waterfallTop = 0; + m_waterfallTop = 0; m_frequencyScaleHeight = fm.height() * 3; // +1 line for marker frequency scale int frequencyScaleTop = 0; - int histogramTop = 0; + m_histogramTop = 0; //int m_leftMargin; m_rightMargin = fm.horizontalAdvance("000"); // displays both histogram and waterfall - if ((m_displayWaterfall || m_display3DSpectrogram) && (m_displayHistogram | m_displayMaxHold | m_displayCurrent)) + if ((m_displayWaterfall || m_display3DSpectrogram) && (m_displayHistogram || m_displayMaxHold || m_displayCurrent)) { m_waterfallHeight = height() * m_waterfallShare - 1; @@ -2758,39 +3384,21 @@ void GLSpectrumView::applyChanges() if (m_invertedWaterfall) { - histogramTop = m_topMargin; + m_histogramTop = m_topMargin; m_histogramHeight = height() - m_topMargin - m_waterfallHeight - m_frequencyScaleHeight - m_bottomMargin; - waterfallTop = histogramTop + m_histogramHeight + m_frequencyScaleHeight + 1; - frequencyScaleTop = histogramTop + m_histogramHeight + 1; + m_waterfallTop = m_histogramTop + m_histogramHeight + m_frequencyScaleHeight + 1; + frequencyScaleTop = m_histogramTop + m_histogramHeight + 1; } else { - waterfallTop = m_topMargin; - frequencyScaleTop = waterfallTop + m_waterfallHeight + 1; - histogramTop = waterfallTop + m_waterfallHeight + m_frequencyScaleHeight + 1; + m_waterfallTop = m_topMargin; + frequencyScaleTop = m_waterfallTop + m_waterfallHeight + 1; + m_histogramTop = m_waterfallTop + m_waterfallHeight + m_frequencyScaleHeight + 1; m_histogramHeight = height() - m_topMargin - m_waterfallHeight - m_frequencyScaleHeight - m_bottomMargin; } m_timeScale.setSize(m_waterfallHeight); - - if (m_sampleRate > 0) - { - float timeScaleDiv = ((float)m_sampleRate / (float)m_timingRate); - - if (m_fftSize > m_fftOverlap) { - timeScaleDiv *= m_fftSize / (float)(m_fftSize - m_fftOverlap); - } - - if (!m_invertedWaterfall) { - m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, (m_waterfallHeight * m_fftSize) / timeScaleDiv, 0); - } else { - m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, 0, (m_waterfallHeight * m_fftSize) / timeScaleDiv); - } - } - else - { - m_timeScale.setRange(Unit::Time, 0, 1); - } + setTimeScaleRange(); m_leftMargin = m_timeScale.getScaleWidth(); @@ -2803,7 +3411,7 @@ void GLSpectrumView::applyChanges() m_glWaterfallBoxMatrix.setToIdentity(); m_glWaterfallBoxMatrix.translate( -1.0f + ((float)(2*m_leftMargin) / (float) width()), - 1.0f - ((float)(2*waterfallTop) / (float) height()) + 1.0f - ((float)(2*m_waterfallTop) / (float) height()) ); m_glWaterfallBoxMatrix.scale( ((float) 2 * (width() - m_leftMargin - m_rightMargin)) / (float) width(), @@ -2813,7 +3421,7 @@ void GLSpectrumView::applyChanges() m_glHistogramBoxMatrix.setToIdentity(); m_glHistogramBoxMatrix.translate( -1.0f + ((float)(2*m_leftMargin) / (float) width()), - 1.0f - ((float)(2*histogramTop) / (float) height()) + 1.0f - ((float)(2*m_histogramTop) / (float) height()) ); m_glHistogramBoxMatrix.scale( ((float) 2 * (width() - m_leftMargin - m_rightMargin)) / (float) width(), @@ -2823,7 +3431,7 @@ void GLSpectrumView::applyChanges() m_glHistogramSpectrumMatrix.setToIdentity(); m_glHistogramSpectrumMatrix.translate( -1.0f + ((float)(2*m_leftMargin) / (float) width()), - 1.0f - ((float)(2*histogramTop) / (float) height()) + 1.0f - ((float)(2*m_histogramTop) / (float) height()) ); m_glHistogramSpectrumMatrix.scale( ((float) 2 * (width() - m_leftMargin - m_rightMargin)) / ((float) width() * (float)(m_nbBins)), @@ -2858,36 +3466,14 @@ void GLSpectrumView::applyChanges() else if (m_displayWaterfall || m_display3DSpectrogram) { m_histogramHeight = 0; - histogramTop = 0; + m_histogramTop = 0; m_bottomMargin = m_frequencyScaleHeight; m_waterfallHeight = height() - m_topMargin - m_frequencyScaleHeight; - waterfallTop = m_topMargin; + m_waterfallTop = m_topMargin; frequencyScaleTop = m_topMargin + m_waterfallHeight + 1; m_timeScale.setSize(m_waterfallHeight); - - if (m_sampleRate > 0) - { - float timeScaleDiv = ((float)m_sampleRate / (float)m_timingRate); - - if (m_fftSize > m_fftOverlap) { - timeScaleDiv *= m_fftSize / (float)(m_fftSize - m_fftOverlap); - } - - if (!m_invertedWaterfall) { - m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, (m_waterfallHeight * m_fftSize) / timeScaleDiv, 0); - } else { - m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, 0, (m_waterfallHeight * m_fftSize) / timeScaleDiv); - } - } - else - { - if (!m_invertedWaterfall) { - m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, 10, 0); - } else { - m_timeScale.setRange(m_timingRate > 1 ? Unit::TimeHMS : Unit::Time, 0, 10); - } - } + setTimeScaleRange(); m_leftMargin = m_timeScale.getScaleWidth(); @@ -2936,7 +3522,7 @@ void GLSpectrumView::applyChanges() { m_bottomMargin = m_frequencyScaleHeight; frequencyScaleTop = height() - m_bottomMargin; - histogramTop = m_topMargin - 1; + m_histogramTop = m_topMargin - 1; m_waterfallHeight = 0; m_histogramHeight = height() - m_topMargin - m_frequencyScaleHeight; @@ -2951,7 +3537,7 @@ void GLSpectrumView::applyChanges() m_glHistogramSpectrumMatrix.setToIdentity(); m_glHistogramSpectrumMatrix.translate( -1.0f + ((float)(2*m_leftMargin) / (float) width()), - 1.0f - ((float)(2*histogramTop) / (float) height()) + 1.0f - ((float)(2*m_histogramTop) / (float) height()) ); m_glHistogramSpectrumMatrix.scale( ((float) 2 * (width() - m_leftMargin - m_rightMargin)) / ((float) width() * (float)(m_nbBins)), @@ -2961,7 +3547,7 @@ void GLSpectrumView::applyChanges() m_glHistogramBoxMatrix.setToIdentity(); m_glHistogramBoxMatrix.translate( -1.0f + ((float)(2*m_leftMargin) / (float) width()), - 1.0f - ((float)(2*histogramTop) / (float) height()) + 1.0f - ((float)(2*m_histogramTop) / (float) height()) ); m_glHistogramBoxMatrix.scale( ((float) 2 * (width() - m_leftMargin - m_rightMargin)) / (float) width(), @@ -3022,7 +3608,7 @@ void GLSpectrumView::applyChanges() { m_histogramRect = QRectF( (float) m_leftMargin / (float) width(), - (float) (waterfallTop + m_waterfallHeight + m_frequencyScaleHeight) / (float) height(), + (float) (m_waterfallTop + m_waterfallHeight + m_frequencyScaleHeight) / (float) height(), (float) (width() - m_leftMargin - m_rightMargin) / (float) width(), (float) m_histogramHeight / (float) height() ); @@ -3055,9 +3641,9 @@ void GLSpectrumView::applyChanges() } // channel overlays - int64_t centerFrequency; + int64_t centerFrequencyUnused; int frequencySpan; - getFrequencyZoom(centerFrequency, frequencySpan); + getFrequencyZoom(centerFrequencyUnused, frequencySpan); for (int i = 0; i < m_channelMarkerStates.size(); ++i) { @@ -3065,7 +3651,7 @@ void GLSpectrumView::applyChanges() qreal xc, pw, nw, dsbw; ChannelMarker::sidebands_t sidebands = dv->m_channelMarker->getSidebands(); - xc = m_centerFrequency + dv->m_channelMarker->getCenterFrequency(); // marker center frequency + xc = centerFrequency + dv->m_channelMarker->getCenterFrequency(); // marker center frequency dsbw = dv->m_channelMarker->getBandwidth(); if (sidebands == ChannelMarker::usb) @@ -3111,7 +3697,7 @@ void GLSpectrumView::applyChanges() dv->m_glMatrixDsbWaterfall = glMatrixDsb; dv->m_glMatrixDsbWaterfall.translate( 0.0f, - (float) waterfallTop / (float) height() + (float) m_waterfallTop / (float) height() ); dv->m_glMatrixDsbWaterfall.scale( (float) (width() - m_leftMargin - m_rightMargin) / (float) width(), @@ -3121,7 +3707,7 @@ void GLSpectrumView::applyChanges() dv->m_glMatrixDsbHistogram = glMatrixDsb; dv->m_glMatrixDsbHistogram.translate( 0.0f, - (float) histogramTop / (float) height() + (float) m_histogramTop / (float) height() ); dv->m_glMatrixDsbHistogram.scale( (float) (width() - m_leftMargin - m_rightMargin) / (float) width(), @@ -3154,7 +3740,7 @@ void GLSpectrumView::applyChanges() dv->m_glMatrixWaterfall = glMatrix; dv->m_glMatrixWaterfall.translate( 0.0f, - (float) waterfallTop / (float) height() + (float) m_waterfallTop / (float) height() ); dv->m_glMatrixWaterfall.scale( (float) (width() - m_leftMargin - m_rightMargin) / (float) width(), @@ -3164,7 +3750,7 @@ void GLSpectrumView::applyChanges() dv->m_glMatrixHistogram = glMatrix; dv->m_glMatrixHistogram.translate( 0.0f, - (float) histogramTop / (float) height() + (float) m_histogramTop / (float) height() ); dv->m_glMatrixHistogram.scale( (float) (width() - m_leftMargin - m_rightMargin) / (float) width(), @@ -3184,9 +3770,9 @@ void GLSpectrumView::applyChanges() /* dv->m_glRect.setRect( - m_frequencyScale.getPosFromValue(m_centerFrequency + dv->m_channelMarker->getCenterFrequency() - dv->m_channelMarker->getBandwidth() / 2) / (float)(width() - m_leftMargin - m_rightMargin), + m_frequencyScale.getPosFromValue(centerFrequency + dv->m_channelMarker->getCenterFrequency() - dv->m_channelMarker->getBandwidth() / 2) / (float)(width() - m_leftMargin - m_rightMargin), 0, - (dv->m_channelMarker->getBandwidth() / (float)m_sampleRate), + (dv->m_channelMarker->getBandwidth() / (float)getDisplayedSampleRate()), 1); */ @@ -3200,7 +3786,7 @@ void GLSpectrumView::applyChanges() /* if(m_displayHistogram || m_displayMaxHold || m_displayWaterfall) { - dv->m_rect.setRect(m_frequencyScale.getPosFromValue(m_centerFrequency + dv->m_channelMarker->getCenterFrequency()) + m_leftMargin - 1, + dv->m_rect.setRect(m_frequencyScale.getPosFromValue(centerFrequency + dv->m_channelMarker->getCenterFrequency()) + m_leftMargin - 1, m_topMargin, 5, height() - m_topMargin - m_bottomMargin); @@ -3209,39 +3795,9 @@ void GLSpectrumView::applyChanges() } // prepare left scales (time and power) - { - m_leftMarginPixmap = QPixmap(m_leftMargin - 1, height()); - m_leftMarginPixmap.fill(Qt::transparent); - { - QPainter painter(&m_leftMarginPixmap); - painter.setPen(QColor(0xf0, 0xf0, 0xff)); - painter.setFont(font()); - const ScaleEngine::TickList* tickList; - const ScaleEngine::Tick* tick; - if (m_displayWaterfall) { - tickList = &m_timeScale.getTickList(); - for (int i = 0; i < tickList->count(); i++) { - tick = &(*tickList)[i]; - if (tick->major) { - if (tick->textSize > 0) - painter.drawText(QPointF(m_leftMargin - M - tick->textSize, waterfallTop + fm.ascent() + tick->textPos), tick->text); - } - } - } - if (m_displayHistogram || m_displayMaxHold || m_displayCurrent) { - tickList = &m_powerScale.getTickList(); - for (int i = 0; i < tickList->count(); i++) { - tick = &(*tickList)[i]; - if (tick->major) { - if (tick->textSize > 0) - painter.drawText(QPointF(m_leftMargin - M - tick->textSize, histogramTop + m_histogramHeight - tick->textPos - 1), tick->text); - } - } - } - } + paintLeftScales(); + m_glShaderLeftScale.initTexture(m_leftMarginPixmap.toImage()); - m_glShaderLeftScale.initTexture(m_leftMarginPixmap.toImage()); - } // prepare frequency scale if (m_displayWaterfall || m_display3DSpectrogram || m_displayHistogram || m_displayMaxHold || m_displayCurrent) { m_frequencyPixmap = QPixmap(width(), m_frequencyScaleHeight); @@ -3277,12 +3833,12 @@ void GLSpectrumView::applyChanges() qreal xc; int shift; //ChannelMarker::sidebands_t sidebands = dv->m_channelMarker->getSidebands(); - xc = m_centerFrequency + dv->m_channelMarker->getCenterFrequency(); // marker center frequency + xc = centerFrequency + dv->m_channelMarker->getCenterFrequency(); // marker center frequency QString ftext; switch (dv->m_channelMarker->getFrequencyScaleDisplayType()) { case ChannelMarker::FScaleDisplay_freq: - ftext = QString::number((m_centerFrequency + dv->m_channelMarker->getCenterFrequency())/1e6, 'f', 6); + ftext = QString::number((centerFrequency + dv->m_channelMarker->getCenterFrequency())/1e6, 'f', 6); break; case ChannelMarker::FScaleDisplay_title: ftext = dv->m_channelMarker->getTitle(); @@ -3294,7 +3850,7 @@ void GLSpectrumView::applyChanges() ftext = dv->m_channelMarker->getDisplayAddressReceive(); break; default: - ftext = QString::number((m_centerFrequency + dv->m_channelMarker->getCenterFrequency())/1e6, 'f', 6); + ftext = QString::number((centerFrequency + dv->m_channelMarker->getCenterFrequency())/1e6, 'f', 6); break; } if (dv->m_channelMarker->getCenterFrequency() < 0) { // left half of scale @@ -3392,17 +3948,28 @@ void GLSpectrumView::applyChanges() painter.setFont(font()); painter.drawText(QPointF(m_leftMargin, fm.height() + fm.ascent() / 2 - 2), infoText); } - m_glShaderInfo.initTexture(m_infoPixmap.toImage()); // Peak details in top info line - QString minFrequencyStr = displayFull(m_centerFrequency - m_sampleRate/2); // This can be wider if negative, while max is positive - QString maxFrequencyStr = displayFull(m_centerFrequency + m_sampleRate/2); + QString minFrequencyStr = displayFull(centerFrequency - getDisplayedSampleRate()/2); // This can be wider if negative, while max is positive + QString maxFrequencyStr = displayFull(centerFrequency + getDisplayedSampleRate()/2); m_peakFrequencyMaxStr = minFrequencyStr.size() > maxFrequencyStr.size() ? minFrequencyStr : maxFrequencyStr; m_peakFrequencyMaxStr = m_peakFrequencyMaxStr.append("Hz"); m_peakPowerMaxStr = m_linear ? "8.000e-10" : "-100.0"; m_peakPowerUnits = m_linear ? "" : "dB"; + m_glShaderSpectrogram.initColorMapTexture(m_colorMapName); + m_glShaderColorMap.initColorMapTexture(m_colorMapName); + m_colorMap = ColorMap::getColorMap(m_colorMapName); + // Why only 240 entries in the palette? + for (int i = 0; i <= 239; i++) + { + ((quint8*)&m_waterfallPalette[i])[0] = (quint8)(m_colorMap[i*3] * 255.0); + ((quint8*)&m_waterfallPalette[i])[1] = (quint8)(m_colorMap[i*3+1] * 255.0); + ((quint8*)&m_waterfallPalette[i])[2] = (quint8)(m_colorMap[i*3+2] * 255.0); + ((quint8*)&m_waterfallPalette[i])[3] = 255; + } + bool waterfallFFTSizeChanged = true; if (m_waterfallBuffer) { @@ -3445,18 +4012,6 @@ void GLSpectrumView::applyChanges() m_3DSpectrogramTexturePos = 0; } - m_glShaderSpectrogram.initColorMapTexture(m_colorMapName); - m_glShaderColorMap.initColorMapTexture(m_colorMapName); - m_colorMap = ColorMap::getColorMap(m_colorMapName); - // Why only 240 entries in the palette? - for (int i = 0; i <= 239; i++) - { - ((quint8*)&m_waterfallPalette[i])[0] = (quint8)(m_colorMap[i*3] * 255.0); - ((quint8*)&m_waterfallPalette[i])[1] = (quint8)(m_colorMap[i*3+1] * 255.0); - ((quint8*)&m_waterfallPalette[i])[2] = (quint8)(m_colorMap[i*3+2] * 255.0); - ((quint8*)&m_waterfallPalette[i])[3] = 255; - } - bool histogramFFTSizeChanged = true; if (m_histogramBuffer) { @@ -3487,6 +4042,21 @@ void GLSpectrumView::applyChanges() std::fill(m_q3ColorMap.m_array, m_q3ColorMap.m_array+4*(m_nbBins+1), 0.0f); } + if (m_redrawAll) + { + redrawSpectrum(); + redrawWaterfallAnd3DSpectrogram(); + m_redrawAll = false; + } + else if (waterfallFFTSizeChanged || windowSizeChanged) + { + redrawWaterfallAnd3DSpectrogram(); + } + else if (histogramFFTSizeChanged) + { + redrawSpectrum(); + } + m_q3TickTime.allocate(4*m_timeScale.getTickList().count()); m_q3TickFrequency.allocate(4*m_frequencyScale.getTickList().count()); m_q3TickPower.allocate(6*m_powerScale.getTickList().count()); // 6 as we need 3d points for 3D spectrogram @@ -3497,13 +4067,13 @@ void GLSpectrumView::applyChanges() void GLSpectrumView::updateHistogramMarkers() { - if (m_sampleRate == 0) { + if (getDisplayedSampleRate() == 0) { return; } int64_t centerFrequency; int frequencySpan; getFrequencyZoom(centerFrequency, frequencySpan); - int effFftSize = m_fftSize * ((float) frequencySpan / (float) m_sampleRate); + int effFftSize = m_fftSize * ((float) frequencySpan / (float) getDisplayedSampleRate()); for (int i = 0; i < m_histogramMarkers.size(); i++) { @@ -3515,7 +4085,7 @@ void GLSpectrumView::updateHistogramMarkers() m_histogramMarkers[i].m_point.ry() = (m_powerScale.getRangeMax() - powerI) / m_powerScale.getRange(); // m_histogramMarkers[i].m_fftBin = - // (((m_histogramMarkers[i].m_frequency - m_centerFrequency) / (float) m_sampleRate) + 0.5) * m_fftSize; + // (((m_histogramMarkers[i].m_frequency - centerFrequency) / (float) getDisplayedSampleRate()) + 0.5) * m_fftSize; m_histogramMarkers[i].m_fftBin = (((m_histogramMarkers[i].m_frequency - centerFrequency) / (float) frequencySpan) + 0.5) * effFftSize; m_histogramMarkers[i].m_point.rx() = m_histogramMarkers[i].m_point.rx() < 0 ? @@ -3530,7 +4100,7 @@ void GLSpectrumView::updateHistogramMarkers() m_histogramMarkers[i].m_frequencyStr = displayScaled( m_histogramMarkers[i].m_frequency, 'f', - getPrecision((m_centerFrequency*1000)/m_sampleRate), + getPrecision((centerFrequency*1000)/getDisplayedSampleRate()), false); m_histogramMarkers[i].m_powerStr = displayPower( powerI, @@ -3543,7 +4113,7 @@ void GLSpectrumView::updateHistogramMarkers() m_histogramMarkers[i].m_deltaFrequencyStr = displayScaled( deltaFrequency, 'f', - getPrecision(deltaFrequency/m_sampleRate), + getPrecision(deltaFrequency/getDisplayedSampleRate()), true); float power0 = m_linear ? m_histogramMarkers.at(0).m_power * (m_useCalibration ? m_calibrationGain : 1.0f) : @@ -3583,7 +4153,7 @@ void GLSpectrumView::updateHistogramPeaks() m_histogramMarkers[i].m_frequencyStr = displayScaled( m_histogramMarkers[i].m_frequency, 'f', - getPrecision((m_centerFrequency*1000)/m_sampleRate), + getPrecision((getDisplayedCenterFrequency()*1000)/getDisplayedSampleRate()), false ); } @@ -3593,7 +4163,7 @@ void GLSpectrumView::updateHistogramPeaks() m_histogramMarkers[i].m_deltaFrequencyStr = displayScaled( deltaFrequency, 'f', - getPrecision(deltaFrequency/m_sampleRate), + getPrecision(deltaFrequency/getDisplayedSampleRate()), true ); } @@ -3624,7 +4194,7 @@ void GLSpectrumView::updateWaterfallMarkers() m_waterfallMarkers[i].m_frequencyStr = displayScaled( m_waterfallMarkers[i].m_frequency, 'f', - getPrecision((m_centerFrequency*1000)/m_sampleRate), + getPrecision((getDisplayedCenterFrequency()*1000)/getDisplayedSampleRate()), false); m_waterfallMarkers[i].m_timeStr = displayScaledF( m_waterfallMarkers[i].m_time, @@ -3638,7 +4208,7 @@ void GLSpectrumView::updateWaterfallMarkers() m_waterfallMarkers.back().m_deltaFrequencyStr = displayScaled( deltaFrequency, 'f', - getPrecision(deltaFrequency/m_sampleRate), + getPrecision(deltaFrequency/getDisplayedSampleRate()), true); m_waterfallMarkers.back().m_deltaTimeStr = displayScaledF( m_waterfallMarkers.at(i).m_time - m_waterfallMarkers.at(0).m_time, @@ -3716,13 +4286,13 @@ void GLSpectrumView::updateCalibrationPoints() QList sortedCalibrationPoints = m_calibrationPoints; std::sort(sortedCalibrationPoints.begin(), sortedCalibrationPoints.end(), calibrationPointsLessThan); - if (m_centerFrequency <= sortedCalibrationPoints.first().m_frequency) + if (getDisplayedCenterFrequency() <= sortedCalibrationPoints.first().m_frequency) { m_calibrationGain = m_calibrationPoints.first().m_powerCalibratedReference / m_calibrationPoints.first().m_powerRelativeReference; m_calibrationShiftdB = CalcDb::dbPower(m_calibrationGain); } - else if (m_centerFrequency >= sortedCalibrationPoints.last().m_frequency) + else if (getDisplayedCenterFrequency() >= sortedCalibrationPoints.last().m_frequency) { m_calibrationGain = m_calibrationPoints.last().m_powerCalibratedReference / m_calibrationPoints.last().m_powerRelativeReference; @@ -3735,7 +4305,7 @@ void GLSpectrumView::updateCalibrationPoints() for (int index = 0; index < sortedCalibrationPoints.size(); index++) { - if (m_centerFrequency < sortedCalibrationPoints[index].m_frequency) + if (getDisplayedCenterFrequency() < sortedCalibrationPoints[index].m_frequency) { highIndex = index; break; @@ -3749,7 +4319,7 @@ void GLSpectrumView::updateCalibrationPoints() // frequency interpolation is always linear double deltaFrequency = sortedCalibrationPoints[highIndex].m_frequency - sortedCalibrationPoints[lowIndex].m_frequency; - double shiftFrequency = m_centerFrequency - sortedCalibrationPoints[lowIndex].m_frequency; + double shiftFrequency = getDisplayedCenterFrequency() - sortedCalibrationPoints[lowIndex].m_frequency; double interpolationRatio = shiftFrequency / deltaFrequency; // calculate low and high gains in linear mode @@ -3792,7 +4362,7 @@ bool GLSpectrumView::event(QEvent* event) { if (pan->state() == Qt::GestureStarted) { - m_scrollStartCenterFreq = m_centerFrequency; + m_scrollStartCenterFreq = getDisplayedCenterFrequency(); } else if (pan->state() == Qt::GestureUpdated) { @@ -3809,7 +4379,7 @@ bool GLSpectrumView::event(QEvent* event) // https://bugreports.qt.io/browse/QTBUG-109205 if (!m_pinching) { - m_scrollStartCenterFreq = m_centerFrequency; + m_scrollStartCenterFreq = getDisplayedCenterFrequency(); m_pinchStart = pinch->centerPoint(); m_pinching = true; m_pinching3D = m_display3DSpectrogram && pointInWaterfallOrSpectrogram(mapFromGlobal(m_pinchStart.toPoint())); @@ -3856,6 +4426,32 @@ bool GLSpectrumView::event(QEvent* event) void GLSpectrumView::mouseMoveEvent(QMouseEvent* event) { + if (m_displayCursorStats) + { + if ((m_displayMaxHold || m_displayCurrent || m_displayHistogram) && pointInHistogram(event->localPos())) + { + m_cursorOverSpectrum = true; + + // Calculate frequency under the cursor + const QPointF& ep = event->localPos(); + QPointF pHis = ep; + pHis.rx() = (ep.x()/width() - m_histogramRect.left()) / m_histogramRect.width(); + pHis.ry() = (ep.y()/height() - m_histogramRect.top()) / m_histogramRect.height(); + m_cursorFrequency = m_frequencyScale.getRangeMin() + pHis.x()*m_frequencyScale.getRange(); + + // Calculate FFT bin under the cursor + float f = (m_cursorFrequency - m_frequencyScale.getRangeMin()) / m_frequencyScale.getRange(); // [0..1] fraction of zoomed frequency range + f += 1.0f / (2.0f * m_nbBins); // Shift by half a bin to get center of bin rather than edge + m_cursorFFTBin = (int) (m_fftMin + f * m_nbBins) % m_fftSize; + + m_displayChanged = true; + } + else + { + m_cursorOverSpectrum = false; + } + } + if (m_rotate3DSpectrogram && !m_pinching3D) { // Rotate 3D Spectrogram @@ -3955,15 +4551,15 @@ void GLSpectrumView::mouseMoveEvent(QMouseEvent* event) // and if so, request an adjustment to the center frequency // FIXME: This doesn't take zoom into account, so only works when zoomed out Real freqAbs = m_frequencyScale.getValueFromPos(event->x() - m_leftMarginPixmap.width() - 1); - Real freqMin = m_centerFrequency - m_sampleRate / 2.0f; - Real freqMax = m_centerFrequency + m_sampleRate / 2.0f; + Real freqMin = getDisplayedCenterFrequency() - getDisplayedSampleRate() / 2.0f; + Real freqMax = getDisplayedCenterFrequency() + getDisplayedSampleRate() / 2.0f; if (freqAbs < freqMin) { - queueRequestCenterFrequency(m_centerFrequency - (freqMin - freqAbs)); + queueRequestCenterFrequency(getDisplayedCenterFrequency() - (freqMin - freqAbs)); } else if (freqAbs > freqMax) { - queueRequestCenterFrequency(m_centerFrequency + (freqAbs - freqMax)); + queueRequestCenterFrequency(getDisplayedCenterFrequency() + (freqAbs - freqMax)); } - Real freq = freqAbs - m_centerFrequency; + Real freq = freqAbs - getDisplayedCenterFrequency(); if (m_channelMarkerStates[m_cursorChannel]->m_channelMarker->getMovable() && (m_channelMarkerStates[m_cursorChannel]->m_channelMarker->getSourceOrSinkStream() == m_displaySourceOrSink) && m_channelMarkerStates[m_cursorChannel]->m_channelMarker->streamIndexApplies(m_displayStreamIndex)) @@ -4031,7 +4627,7 @@ void GLSpectrumView::mousePressEvent(QMouseEvent* event) if ((event->button() == Qt::MiddleButton) && (m_displayMaxHold || m_displayCurrent || m_displayHistogram) && pointInHistogram(ep)) { m_scrollFrequency = true; - m_scrollStartCenterFreq = m_centerFrequency; + m_scrollStartCenterFreq = getDisplayedCenterFrequency(); m_mousePrevLocalPos = ep; return; } @@ -4122,7 +4718,7 @@ void GLSpectrumView::mousePressEvent(QMouseEvent* event) float frequency = m_frequencyScale.getRangeMin() + pHis.x()*m_frequencyScale.getRange(); float powerVal = m_powerScale.getRangeMax() - pHis.y()*m_powerScale.getRange(); float power = m_linear ? powerVal : CalcDb::powerFromdB(powerVal); - int fftBin = (((frequency - m_centerFrequency) / (float) m_sampleRate) * m_fftSize) + (m_fftSize / 2); + int fftBin = (((frequency - getDisplayedCenterFrequency()) / (float) getDisplayedSampleRate()) * m_fftSize) + (m_fftSize / 2); if ((pHis.x() >= 0) && (pHis.x() <= 1) && (pHis.y() >= 0) && (pHis.y() <= 1)) { @@ -4135,7 +4731,7 @@ void GLSpectrumView::mousePressEvent(QMouseEvent* event) m_histogramMarkers.back().m_frequencyStr = displayScaled( frequency, 'f', - getPrecision((m_centerFrequency*1000)/m_sampleRate), + getPrecision((getDisplayedCenterFrequency()*1000)/getDisplayedSampleRate()), false); m_histogramMarkers.back().m_power = power; m_histogramMarkers.back().m_powerStr = displayPower( @@ -4149,7 +4745,7 @@ void GLSpectrumView::mousePressEvent(QMouseEvent* event) m_histogramMarkers.back().m_deltaFrequencyStr = displayScaled( deltaFrequency, 'f', - getPrecision(deltaFrequency/m_sampleRate), + getPrecision(deltaFrequency/getDisplayedSampleRate()), true); float power0 = m_linear ? m_histogramMarkers.at(0).m_power : @@ -4183,7 +4779,7 @@ void GLSpectrumView::mousePressEvent(QMouseEvent* event) m_waterfallMarkers.back().m_frequencyStr = displayScaled( frequency, 'f', - getPrecision((m_centerFrequency*1000)/m_sampleRate), + getPrecision((getDisplayedCenterFrequency()*1000)/getDisplayedSampleRate()), false); m_waterfallMarkers.back().m_time = time; m_waterfallMarkers.back().m_timeStr = displayScaledF( @@ -4198,7 +4794,7 @@ void GLSpectrumView::mousePressEvent(QMouseEvent* event) m_waterfallMarkers.back().m_deltaFrequencyStr = displayScaled( deltaFrequency, 'f', - getPrecision(deltaFrequency/m_sampleRate), + getPrecision(deltaFrequency/getDisplayedSampleRate()), true); m_waterfallMarkers.back().m_deltaTimeStr = displayScaledF( time - m_waterfallMarkers.at(0).m_time, @@ -4306,7 +4902,7 @@ void GLSpectrumView::mousePressEvent(QMouseEvent* event) setCursor(Qt::SizeHorCursor); m_cursorState = CSChannelMoving; m_cursorChannel = 0; - Real freq = m_frequencyScale.getValueFromPos(event->x() - m_leftMarginPixmap.width() - 1) - m_centerFrequency; + Real freq = m_frequencyScale.getValueFromPos(event->x() - m_leftMarginPixmap.width() - 1) - getDisplayedCenterFrequency(); if (m_channelMarkerStates[m_cursorChannel]->m_channelMarker->getMovable() && (m_channelMarkerStates[m_cursorChannel]->m_channelMarker->getSourceOrSinkStream() == m_displaySourceOrSink) @@ -4383,7 +4979,7 @@ void GLSpectrumView::zoomFactor(const QPointF& p, float factor) float zoomFreq = m_frequencyScale.getRangeMin() + pwx*m_frequencyScale.getRange(); // Calculate current centre frequency - float currentCF = (m_frequencyZoomFactor == 1) ? m_centerFrequency : ((m_frequencyZoomPos - 0.5) * m_sampleRate + m_centerFrequency); + float currentCF = (m_frequencyZoomFactor == 1) ? getDisplayedCenterFrequency() : ((m_frequencyZoomPos - 0.5) * getDisplayedSampleRate() + getDisplayedCenterFrequency()); // Calculate difference from frequency under cursor to centre frequency float freqDiff = (currentCF - zoomFreq); @@ -4401,16 +4997,18 @@ void GLSpectrumView::zoomFactor(const QPointF& p, float factor) float zoomedCF = zoomFreq + zoomedFreqDiff; // Calculate zoom position which will set the desired center frequency - float zoomPos = (zoomedCF - m_centerFrequency) / m_sampleRate + 0.5; + float zoomPos = (zoomedCF - getDisplayedCenterFrequency()) / getDisplayedSampleRate() + 0.5; zoomPos = std::max(0.0f, zoomPos); zoomPos = std::min(1.0f, zoomPos); frequencyZoom(zoomPos); } - } +} void GLSpectrumView::zoom(const QPointF& p, int y) { + QMutexLocker mutexLocker(&m_mutex); // Lock to ensure we don't try to redraw mid calculation of zoom and fftMin/fftMax + float pwx = (p.x() - m_leftMargin) / (width() - m_leftMargin - m_rightMargin); // x position in window if ((pwx >= 0.0f) && (pwx <= 1.0f)) @@ -4421,8 +5019,8 @@ void GLSpectrumView::zoom(const QPointF& p, int y) float zoomFreq = m_frequencyScale.getRangeMin() + pwx*m_frequencyScale.getRange(); // Calculate current centre frequency - int adjSampleRate = m_ssbSpectrum ? m_sampleRate/2 : m_sampleRate; - qint64 adjCenterFrequency = m_centerFrequency + (m_ssbSpectrum ? m_sampleRate/4 : 0); + int adjSampleRate = m_ssbSpectrum ? getDisplayedSampleRate()/2 : getDisplayedSampleRate(); + qint64 adjCenterFrequency = getDisplayedCenterFrequency() + (m_ssbSpectrum ? getDisplayedSampleRate()/4 : 0); float currentCF = (m_frequencyZoomFactor == 1) ? adjCenterFrequency : (m_frequencyZoomPos - 0.5) * adjSampleRate + adjCenterFrequency; @@ -4491,7 +5089,8 @@ void GLSpectrumView::zoom(const QPointF& p, int y) void GLSpectrumView::frequencyZoom(float zoomPos) { m_frequencyZoomPos = zoomPos; - updateFFTLimits(); + updateFFTLimits(false); + m_displayChanged = true; } void GLSpectrumView::frequencyPan(QMouseEvent *event) @@ -4509,7 +5108,7 @@ void GLSpectrumView::frequencyPan(QMouseEvent *event) m_frequencyZoomPos = m_frequencyZoomPos < lim ? lim : m_frequencyZoomPos > 1 - lim ? 1 - lim : m_frequencyZoomPos; qDebug("GLSpectrumView::frequencyPan: pw: %f p: %f", pw, m_frequencyZoomPos); - updateFFTLimits(); + updateFFTLimits(false); } void GLSpectrumView::timeZoom(bool zoomInElseOut) @@ -4540,9 +5139,11 @@ void GLSpectrumView::powerZoom(float pw, bool zoomInElseOut) m_referenceLevel = m_referenceLevel + (zoomInElseOut ? -1 : 1); } // top - m_powerRange = m_powerRange < 1 ? 1 : m_powerRange > 100 ? 100 : m_powerRange; - m_referenceLevel = m_referenceLevel < -110 ? -110 : m_referenceLevel > 0 ? 0 : m_referenceLevel; + m_powerRange = std::clamp(m_powerRange, m_minPowerRange, m_maxPowerRange); + m_referenceLevel = std::clamp(m_referenceLevel, m_minReferenceLevel, m_maxReferenceLevel); m_changesPending = true; + m_displayChanged = true; + redrawWaterfallAnd3DSpectrogram(); if (m_messageQueueToGUI) { m_messageQueueToGUI->push(new MsgReportPowerScale(m_referenceLevel, m_powerRange)); @@ -4554,25 +5155,32 @@ void GLSpectrumView::resetFrequencyZoom() m_frequencyZoomFactor = 1.0f; m_frequencyZoomPos = 0.5f; - updateFFTLimits(); + updateFFTLimits(false); } -void GLSpectrumView::updateFFTLimits() +void GLSpectrumView::updateFFTLimits(bool fftSizeChangedOnly) { - if (m_spectrumVis) + if (!fftSizeChangedOnly) { - m_spectrumVis->getInputMessageQueue()->push(SpectrumVis::MsgFrequencyZooming::create( - m_frequencyZoomFactor, m_frequencyZoomPos - )); + if (m_messageQueueToGUI) + { + m_messageQueueToGUI->push(GLSpectrumView::MsgFrequencyZooming::create( + m_frequencyZoomFactor, m_frequencyZoomPos + )); + } } - if (m_messageQueueToGUI) - { - m_messageQueueToGUI->push(SpectrumVis::MsgFrequencyZooming::create( - m_frequencyZoomFactor, m_frequencyZoomPos - )); + m_fftMin = m_frequencyZoomFactor == 1.0f ? 0 : (m_frequencyZoomPos - (0.5f / m_frequencyZoomFactor)) * m_fftSize; + m_fftMax = m_frequencyZoomFactor == 1.0f ? m_fftSize : (m_frequencyZoomPos + (0.5f / m_frequencyZoomFactor)) * m_fftSize; + if (m_fftMin < 0) { + m_fftMin = 0; } + if (m_fftMax > m_fftSize) { + m_fftMax = m_fftSize; + } + m_nbBins = m_fftMax - m_fftMin; + m_redrawAll = true; m_changesPending = true; } @@ -4582,6 +5190,18 @@ void GLSpectrumView::setFrequencyZooming(float frequencyZoomFactor, float freque frequencyZoom(frequencyZoomPos); } +void GLSpectrumView::setReferenceLevelRange(Real minReferenceLevel, Real maxReferenceLevel) +{ + m_minReferenceLevel = minReferenceLevel; + m_maxReferenceLevel = maxReferenceLevel; +} + +void GLSpectrumView::setPowerRangeRange(Real minPowerRange, Real maxPowerRange) +{ + m_minPowerRange = minPowerRange; + m_maxPowerRange = maxPowerRange; +} + void GLSpectrumView::setFrequencyScale() { int frequencySpan; @@ -4615,20 +5235,14 @@ void GLSpectrumView::setPowerScale(int height) void GLSpectrumView::getFrequencyZoom(int64_t& centerFrequency, int& frequencySpan) { - int adjSampleRate = m_ssbSpectrum ? m_sampleRate/2 : m_sampleRate; - qint64 adjCenterFrequency = m_centerFrequency + (m_ssbSpectrum ? m_sampleRate/4 : 0); + int adjSampleRate = m_ssbSpectrum ? getDisplayedSampleRate()/2 : getDisplayedSampleRate(); + qint64 adjCenterFrequency = getDisplayedCenterFrequency() + (m_ssbSpectrum ? getDisplayedSampleRate()/4 : 0); frequencySpan = (m_frequencyZoomFactor == 1) ? adjSampleRate : adjSampleRate * (1.0 / m_frequencyZoomFactor); centerFrequency = (m_frequencyZoomFactor == 1) ? adjCenterFrequency : (m_frequencyZoomPos - 0.5) * adjSampleRate + adjCenterFrequency; } -// void GLSpectrumView::updateFFTLimits() -// { -// m_fftMin = m_frequencyZoomFactor == 1 ? 0 : (m_frequencyZoomPos - (0.5f / m_frequencyZoomFactor)) * m_fftSize; -// m_fftMax = m_frequencyZoomFactor == 1 ? m_fftSize : (m_frequencyZoomPos - (0.5f / m_frequencyZoomFactor)) * m_fftSize; -// } - void GLSpectrumView::channelMarkerMove(QWheelEvent *event, int mul) { for (int i = 0; i < m_channelMarkerStates.size(); ++i) @@ -4650,7 +5264,7 @@ void GLSpectrumView::channelMarkerMove(QWheelEvent *event, int mul) } // calculate scale relative cursor position for new frequency - float x_pos = m_frequencyScale.getPosFromValue(m_centerFrequency + freq); + float x_pos = m_frequencyScale.getPosFromValue(getDisplayedCenterFrequency() + freq); if ((x_pos >= 0.0) && (x_pos < m_frequencyScale.getSize())) // cursor must be in scale { @@ -4713,6 +5327,9 @@ void GLSpectrumView::leaveEvent(QEvent* event) void GLSpectrumView::tick() { + // Update scroll bar to account for changes to m_spectrumBuffer and waterfall size changes + updateScrollBar(); + if (m_displayChanged) { m_displayChanged = false; @@ -4919,18 +5536,21 @@ void GLSpectrumView::drawTextsRight(const QStringList &text, const QStringList & int textWidth, maxWidth; for (int i = text.length() - 1; i >= 0; i--) { - textWidth = fm.horizontalAdvance(units[i]); - painter.drawText(QPointF(x - textWidth, y), units[i]); - x -= textWidth; + if (!text[i].isEmpty() || !value[i].isEmpty()) + { + textWidth = fm.horizontalAdvance(units[i]); + painter.drawText(QPointF(x - textWidth, y), units[i]); + x -= textWidth; - textWidth = fm.horizontalAdvance(value[i]); - maxWidth = fm.horizontalAdvance(max[i]); - painter.drawText(QPointF(x - textWidth, y), value[i]); - x -= maxWidth; + textWidth = fm.horizontalAdvance(value[i]); + maxWidth = fm.horizontalAdvance(max[i]); + painter.drawText(QPointF(x - textWidth, y), value[i]); + x -= maxWidth; - textWidth = fm.horizontalAdvance(text[i]); - painter.drawText(QPointF(x - textWidth, y), text[i]); - x -= textWidth; + textWidth = fm.horizontalAdvance(text[i]); + painter.drawText(QPointF(x - textWidth, y), text[i]); + x -= textWidth; + } } m_glShaderTextOverlay.initTexture(m_infoPixmap.toImage()); @@ -5065,26 +5685,110 @@ void GLSpectrumView::drawTextOverlay( void GLSpectrumView::formatTextInfo(QString& info) { + QString spacing = " "; + if (m_useCalibration) { - info.append(tr("CAL:%1dB ").arg(QString::number(m_calibrationShiftdB, 'f', 1))); + info.append(tr("CAL:%1dB%2").arg(QString::number(m_calibrationShiftdB, 'f', 1)).arg(spacing)); } if (m_frequencyZoomFactor != 1.0f) { - info.append(tr("%1x ").arg(QString::number(m_frequencyZoomFactor, 'f', 1))); + info.append(tr("%1x%2").arg(QString::number(m_frequencyZoomFactor, 'f', 1)).arg(spacing)); } - if (m_sampleRate == 0) + if (getDisplayedSampleRate() == 0) { - info.append(tr("CF:%1 SP:%2").arg(m_centerFrequency).arg(m_sampleRate)); + info.append(tr("CF:%1 SP:%2%3").arg(getDisplayedCenterFrequency()).arg(getDisplayedSampleRate()).arg(spacing)); } else { int64_t centerFrequency; int frequencySpan; getFrequencyZoom(centerFrequency, frequencySpan); - info.append(tr("CF:%1 ").arg(displayScaled(centerFrequency, 'f', getPrecision(centerFrequency/frequencySpan), true))); - info.append(tr("SP:%1 ").arg(displayScaled(frequencySpan, 'f', 3, true))); + info.append(tr("CF:%1%2").arg(displayScaled(centerFrequency, 'f', getPrecision(centerFrequency/frequencySpan), true)).arg(spacing)); + info.append(tr("SP:%1%2").arg(displayScaled(frequencySpan, 'f', 3, true)).arg(spacing)); } + + if (m_displayRBW) + { + float rbw = getDisplayedSampleRate() / (float) m_fftSize; + info.append(tr("RBW:%1%2").arg(displayScaled(rbw, 'f', 3, true)).arg(spacing)); + } +} + +// Paint right side of status line, which contains peak power/freq and power/freq under cursor +void GLSpectrumView::paintStatusLineRight() +{ + QString cursorInfo; + QString cursorPower; + QString cursorFreq; + QString cursorPowerUnits; + QString cursorFreqUnits; + + if (m_displayCursorStats && m_cursorOverSpectrum && m_currentSpectrum && (m_cursorFFTBin < m_fftSize)) + { + Real power = m_currentSpectrum[m_cursorFFTBin]; + + cursorInfo = tr("Cur: "); + cursorPower = displayPower(power, m_linear ? 'e' : 'f', m_linear ? 3 : 1); + cursorFreq = displayFull(m_cursorFrequency); + cursorPowerUnits = m_peakPowerUnits; + cursorFreqUnits = "Hz"; + } + + QString peakInfo; + QString peakPower; + QString peakFreq; + QString peakPowerUnits; + QString peakFreqUnits; + + if (m_displayPeakStats && m_currentSpectrum) + { + float peakPowerValue; + float peakFreqValue; + findPeak(&m_currentSpectrum[m_fftMin], peakPowerValue, peakFreqValue); + peakInfo = tr("Pk: "); + peakPower = displayPower(peakPowerValue, m_linear ? 'e' : 'f', m_linear ? 3 : 1); + peakFreq = displayFull(peakFreqValue); + peakPowerUnits = m_peakPowerUnits; + peakFreqUnits = "Hz"; + } + + if (m_displayCursorStats || m_displayPeakStats) + { + QString spacing = m_displayCursorStats && m_displayPeakStats ? " " : ""; + + drawTextsRight( + { + cursorInfo, + "", + spacing, + peakInfo, + "" + }, + { + cursorPower, + cursorFreq, + "", + peakPower, + peakFreq + }, + { + m_peakPowerMaxStr, + m_peakFrequencyMaxStr, + "", + m_peakPowerMaxStr, + m_peakFrequencyMaxStr + }, + { + cursorPowerUnits, + cursorFreqUnits, + "", + peakPowerUnits, + peakFreqUnits + } + ); + } + } bool GLSpectrumView::eventFilter(QObject *object, QEvent *event) @@ -5172,3 +5876,24 @@ bool GLSpectrumView::eventFilter(QObject *object, QEvent *event) return QOpenGLWidget::eventFilter(object, event); } } + +void GLSpectrumView::setMemory(int memoryIdx, const SpectrumSettings::SpectrumMemory &memory) +{ + QMutexLocker mutexLocker(&m_mutex); + m_spectrumMemory[memoryIdx] = memory; + m_displayChanged = true; +} + +void GLSpectrumView::getDisplayedSpectrumCopy(std::vector& copy, bool zoomed) +{ + QMutexLocker mutexLocker(&m_mutex); + + if (m_currentSpectrum) + { + if (zoomed) { + copy.assign(m_currentSpectrum + m_fftMin, m_currentSpectrum + m_fftMax); + } else { + copy.assign(m_currentSpectrum, m_currentSpectrum + m_fftSize); + } + } +} diff --git a/sdrgui/gui/glspectrumview.h b/sdrgui/gui/glspectrumview.h index 93ffd077c..c06107d50 100644 --- a/sdrgui/gui/glspectrumview.h +++ b/sdrgui/gui/glspectrumview.h @@ -3,7 +3,7 @@ // written by Christian Daniel // // Copyright (C) 2015-2022 Edouard Griffiths, F4EXB // // Copyright (C) 2018 beta-tester // -// Copyright (C) 2022-2023 Jon Beniston, M7RCE // +// Copyright (C) 2022-2026 Jon Beniston, M7RCE // // Copyright (C) 2022 Jiří Pinkava // // // // OpenGL interface modernization. // @@ -34,6 +34,8 @@ #include #include #include +#include +#include #include "gui/qtcompatibility.h" #include "gui/scaleengine.h" #include "gui/glshadersimple.h" @@ -47,6 +49,7 @@ #include "export.h" #include "util/incrementalarray.h" #include "util/message.h" +#include "util/circularbuffer.h" #include "util/colormap.h" #include "util/peakfinder.h" @@ -56,7 +59,7 @@ class SpectrumVis; class QOpenGLDebugLogger; class SpectrumMeasurements; -class SDRGUI_API GLSpectrumView : public QOpenGLWidget, public GLSpectrumInterface { +class SDRGUI_API GLSpectrumView : public QOpenGLWidget, public GLSpectrumInterface, public ScaleEngine::TickFormatter { Q_OBJECT public: @@ -155,6 +158,28 @@ public: {} }; + class MsgFrequencyZooming : public Message { + MESSAGE_CLASS_DECLARATION + + public: + float getFrequencyZoomFactor() const { return m_frequencyZoomFactor; } + float getFrequencyZoomPos() const { return m_frequencyZoomPos; } + + static MsgFrequencyZooming* create(float frequencyZoomFactor, float frequencyZoomPos) { + return new MsgFrequencyZooming(frequencyZoomFactor, frequencyZoomPos); + } + + private: + float m_frequencyZoomFactor; + float m_frequencyZoomPos; + + MsgFrequencyZooming(float frequencyZoomFactor, float frequencyZoomPos) : + Message(), + m_frequencyZoomFactor(frequencyZoomFactor), + m_frequencyZoomPos(frequencyZoomPos) + { } + }; + GLSpectrumView(QWidget* parent = nullptr); virtual ~GLSpectrumView(); @@ -166,13 +191,16 @@ public: void setTimingRate(qint32 timingRate); void setFFTOverlap(int overlap); void setReferenceLevel(Real referenceLevel); + void setReferenceLevelRange(Real minReferenceLevel, Real maxReferenceLevel); void setPowerRange(Real powerRange); + void setPowerRangeRange(Real minPowerRange, Real maxPowerRange); void setDecay(int decay); void setDecayDivisor(int decayDivisor); void setHistoStroke(int stroke); void setDisplayWaterfall(bool display); void setDisplay3DSpectrogram(bool display); void set3DSpectrogramStyle(SpectrumSettings::SpectrogramStyle style); + void setSpectrumColor(QRgb color); void setColorMapName(const QString &colorMapName); void setSpectrumStyle(SpectrumSettings::SpectrumStyle style); void setSsbSpectrum(bool ssbSpectrum); @@ -190,14 +218,15 @@ public: void setMeasurements(SpectrumMeasurements *measurements) { m_measurements = measurements; } void setMeasurementParams(SpectrumSettings::Measurement measurement, int centerFrequencyOffset, int bandwidth, int chSpacing, int adjChBandwidth, - int harmonics, int peaks, bool highlight, int precision); + int harmonics, int peaks, bool highlight, int precision, unsigned memoryMask); + void resetMeasurements(); qint32 getSampleRate() const { return m_sampleRate; } void addChannelMarker(ChannelMarker* channelMarker); void removeChannelMarker(ChannelMarker* channelMarker); void setMessageQueueToGUI(MessageQueue* messageQueue) { m_messageQueueToGUI = messageQueue; } - virtual void newSpectrum(const Real* spectrum, int nbBins, int fftSize); + virtual void newSpectrum(const Real* spectrum, int fftSize); void clearSpectrumHistogram(); Real getWaterfallShare() const { return m_waterfallShare; } @@ -237,6 +266,17 @@ public: void setIsDeviceSpectrum(bool isDeviceSpectrum) { m_isDeviceSpectrum = isDeviceSpectrum; } bool isDeviceSpectrum() const { return m_isDeviceSpectrum; } void setFrequencyZooming(float frequencyZoomFactor, float frequencyZoomPos); + void setWaterfallTimeFormat(SpectrumSettings::WaterfallTimeUnits waterfallTimeUnits, const QString& format); + void setStatusLine(bool displayRBW, bool displayCursorStats, bool displayPeakStats); + void setScrolling(bool enabled, int length); + void setScrollBar(QScrollBar* scrollBar); + void readCSV(QTextStream &in, bool append, QString &error); + void writeCSV(QTextStream &out); + bool writeImage(const QString& filename); + void getDisplayedSpectrumCopy(std::vector& copy, bool zoomed); + void setMemory(int memoryIdx, const SpectrumSettings::SpectrumMemory &memory); + + QString formatTick(double value) const override; private: struct ChannelMarkerState { @@ -282,10 +322,15 @@ private: QMutex m_mutex; bool m_mouseInside; bool m_changesPending; + bool m_redrawAll; // Call redrawSpectrum/redrawWaterfallAnd3DSpectrogram in applyChanges() qint64 m_centerFrequency; Real m_referenceLevel; + Real m_minReferenceLevel; + Real m_maxReferenceLevel; Real m_powerRange; + Real m_minPowerRange; + Real m_maxPowerRange; bool m_linear; int m_decay; quint32 m_sampleRate; @@ -294,6 +339,8 @@ private: int m_fftSize; //!< FFT size in number of bins int m_nbBins; //!< Number of visible FFT bins (zoom support) + int m_fftMin; //!< First bin when zooming in + int m_fftMax; //!< Last bin when zooming in bool m_displayGrid; int m_displayGridIntensity; @@ -315,6 +362,8 @@ private: int m_histogramHeight; int m_waterfallHeight; int m_bottomMargin; + int m_waterfallTop; + int m_histogramTop; QFont m_textOverlayFont; QPixmap m_leftMarginPixmap; QPixmap m_frequencyPixmap; @@ -357,6 +406,7 @@ private: QPixmap m_spectrogramTimePixmap; QPixmap m_spectrogramPowerPixmap; SpectrumSettings::SpectrogramStyle m_3DSpectrogramStyle; + QColor m_spectrumColor; QString m_colorMapName; SpectrumSettings::SpectrumStyle m_spectrumStyle; const float *m_colorMap; @@ -426,16 +476,56 @@ private: int m_measurementPeaks; bool m_measurementHighlight; int m_measurementPrecision; + int m_measurementMemMasks; + std::vector m_maskTestCount; + std::vector m_maskFailCount; + std::vector> m_maskFails; static const QVector4D m_measurementLightMarkerColor; static const QVector4D m_measurementDarkMarkerColor; + bool m_displayRBW; + bool m_displayCursorStats; + bool m_displayPeakStats; + bool m_cursorOverSpectrum; + float m_cursorFrequency; + int m_cursorFFTBin; + + std::vector m_spectrumNoBuffer; // Copy of most recent spectrum, when scroll buffer disabled + + // Spectrum scroll buffer + + struct Spectrum { + Real *m_spectrum; + quint32 m_sampleRate; + qint64 m_centerFrequency; + QDateTime m_dateTime; + }; + + CircularBuffer m_spectrumBuffer; + int m_spectrumBufferFFTSize; + int m_spectrumBufferMaxSize; + QScrollBar* m_scrollBar; + bool m_scrollBarEnabled; + int m_scrollBarValue; + + SpectrumSettings::WaterfallTimeUnits m_waterfallTimeUnits; + QString m_waterfallTimeFormat; + + QVector m_spectrumMemory; + #ifdef ENABLE_PROFILER QString m_profileName; #endif - void updateWaterfall(const Real *spectrum); - void update3DSpectrogram(const Real *spectrum); - void updateHistogram(const Real *spectrum); + void newSpectrum(const Real* spectrum, int fftSize, quint32 sampleRate, qint64 centerFrequency, const QDateTime &dateTime); + void updateSpectrumNoBuffer(const Real *spectrum, int fftSize); + void clearSpectrumBuffer(); + void updateSpectrumBuffer(const Real *spectrum, int fftSize, quint32 sampleRate, qint64 centerFrequency, const QDateTime &dateTime); + void clearWaterfallRow(int nbBins); + void updateWaterfall(const Real *spectrum, int fftSize, int fftMin, int nbBins); + void clear3DSpectrogramRow(int nbBins); + void update3DSpectrogram(const Real *spectrum, int fftSize, int fftMin, int nbBins); + void updateHistogram(const Real *spectrum, int fftMin, int nbBins); void initializeGL(); void resizeGL(int width, int height); @@ -446,18 +536,19 @@ private: void drawSpectrumMarkers(); void drawAnnotationMarkers(); - void measurePeak(); - void measurePeaks(); - void measureChannelPower(); - void measureAdjacentChannelPower(); - void measureOccupiedBandwidth(); - void measure3dBBandwidth(); - void measureSNR(); - void measureSFDR(); - float calcChannelPower(int64_t centerFrequency, int channelBandwidth) const; + void measure(const Real *spectrum, bool updateGUI); + void measurePeaks(const Real *spectrum); + void measureChannelPower(const Real *spectrum, bool updateGUI); + void measureAdjacentChannelPower(const Real *spectrum, bool updateGUI); + void measureOccupiedBandwidth(const Real *spectrum, bool updateGUI); + void measure3dBBandwidth(const Real *spectrum, bool updateGUI); + void measureSNR(const Real *spectrum, bool updateGUI); + void measureSFDR(const Real *spectrum, bool updateGUI); + void measureMask(const Real *spectrum, int fftSize, bool updateGUI); + float calcChannelPower(const Real *spectrum, int64_t centerFrequency, int channelBandwidth) const; float calPower(float power) const; int findPeakBin(const Real *spectrum) const; - void findPeak(float &power, float &frequency) const; + void findPeak(const Real *spectrum, float &power, float &frequency) const; void peakWidth(const Real *spectrum, int center, int &left, int &right, int maxLeft, int maxRight) const; int frequencyToBin(int64_t frequency) const; int64_t binToFrequency(int bin) const; @@ -478,7 +569,7 @@ private: void timeZoom(bool zoomInElseOut); void powerZoom(float pw, bool zoomInElseOut); void resetFrequencyZoom(); - void updateFFTLimits(); + void updateFFTLimits(bool fftSizeOnly); void setFrequencyScale(); void setPowerScale(int height); void getFrequencyZoom(int64_t& centerFrequency, int& frequencySpan); @@ -515,6 +606,16 @@ private: void updateSortedAnnotationMarkers(); void queueRequestCenterFrequency(qint64 frequency); + void setTimeScaleRange(); + void redrawSpectrum(); + void redrawWaterfallAnd3DSpectrogram(); + void paintLeftScales(); + void paintStatusLineRight(); + void updateScrollBar(); + int scrollBarValue() const; + qint64 getDisplayedCenterFrequency() const; + quint32 getDisplayedSampleRate() const; + static bool annotationDisplayLessThan(const SpectrumAnnotationMarker *m1, const SpectrumAnnotationMarker *m2) { if (m1->m_bandwidth == m2->m_bandwidth) { @@ -529,6 +630,21 @@ private: return m1.m_frequency < m2.m_frequency; } + inline Real clampPower(Real value) const + { + return std::clamp(value, -m_powerRange, 0.0f); + } + + inline int clampWaterfall(int value) const + { + return std::clamp(value, 0, 239); + } + + inline int clampPixel(int value) const + { + return std::clamp(value, 0, 255); + } + private slots: void cleanup(); void tick(); @@ -536,6 +652,7 @@ private slots: void channelMarkerDestroyed(QObject* object); void openGLDebug(const QOpenGLDebugMessage &debugMessage); bool eventFilter(QObject *object, QEvent *event); + void scrollBarValueChanged(int value); signals: // Emitted when user tries to scroll to frequency currently out of range diff --git a/sdrgui/gui/profiledialog.cpp b/sdrgui/gui/profiledialog.cpp index 25127fe41..24f319a1c 100644 --- a/sdrgui/gui/profiledialog.cpp +++ b/sdrgui/gui/profiledialog.cpp @@ -59,6 +59,7 @@ void ProfileDialog::resizeTable() ui->table->setItem(row, COL_TOTAL, new QTableWidgetItem("1000.000 ms")); ui->table->setItem(row, COL_AVERAGE, new QTableWidgetItem("1000.000 ns/frame")); ui->table->setItem(row, COL_LAST, new QTableWidgetItem("1000.000 ms")); + ui->table->setItem(row, COL_MAX, new QTableWidgetItem("1000.000 ms")); ui->table->setItem(row, COL_NUM_SAMPLES, new QTableWidgetItem("1000000000")); ui->table->resizeColumnsToContents(); ui->table->setRowCount(row); @@ -79,6 +80,7 @@ void ProfileDialog::updateData() double totalTime = data.getTotal(); double averageTime = data.getAverage(); double lastTime = data.getLast(); + double maxTime = data.getMax(); int i = 0; for (; i < ui->table->rowCount(); i++) @@ -90,6 +92,7 @@ void ProfileDialog::updateData() ui->table->item(i, COL_TOTAL)->setData(Qt::DisplayRole, totalTime); ui->table->item(i, COL_AVERAGE)->setData(Qt::DisplayRole, averageTime); ui->table->item(i, COL_LAST)->setData(Qt::DisplayRole, lastTime); + ui->table->item(i, COL_MAX)->setData(Qt::DisplayRole, maxTime); ui->table->item(i, COL_NUM_SAMPLES)->setData(Qt::DisplayRole, data.getNumSamples()); break; } @@ -105,31 +108,47 @@ void ProfileDialog::updateData() QTableWidgetItem *total = new QTableWidgetItem(); QTableWidgetItem *average = new QTableWidgetItem(); QTableWidgetItem *last = new QTableWidgetItem(); + QTableWidgetItem *max = new QTableWidgetItem(); QTableWidgetItem *numSamples = new QTableWidgetItem(); ui->table->setItem(row, COL_NAME, name); ui->table->setItem(row, COL_TOTAL, total); ui->table->setItem(row, COL_AVERAGE, average); ui->table->setItem(row, COL_LAST, last); + ui->table->setItem(row, COL_MAX, max); ui->table->setItem(row, COL_NUM_SAMPLES, numSamples); total->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); average->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); last->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); + max->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); numSamples->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); total->setData(Qt::DisplayRole, totalTime); average->setData(Qt::DisplayRole, averageTime); last->setData(Qt::DisplayRole, lastTime); + max->setData(Qt::DisplayRole, maxTime); numSamples->setData(Qt::DisplayRole, data.getNumSamples()); ui->table->setItemDelegateForColumn(COL_TOTAL, new NanoSecondsDelegate()); ui->table->setItemDelegateForColumn(COL_AVERAGE, new NanoSecondsDelegate()); ui->table->setItemDelegateForColumn(COL_LAST, new NanoSecondsDelegate()); + ui->table->setItemDelegateForColumn(COL_MAX, new NanoSecondsDelegate()); ui->table->setSortingEnabled(true); } } GlobalProfileData::releaseProfileData(); + + qint64 msecSinceStart = GlobalProfileData::getMSSinceStart(); + QString s; + + if (msecSinceStart < 1e3) { + s = QString("%1 ms").arg(msecSinceStart); + } else { + s = QString("%1 s").arg(msecSinceStart/1e3, 0, 'f', 3); + } + ui->time->setText(s); + } diff --git a/sdrgui/gui/profiledialog.h b/sdrgui/gui/profiledialog.h index a6a91e909..9cd2c2a04 100644 --- a/sdrgui/gui/profiledialog.h +++ b/sdrgui/gui/profiledialog.h @@ -52,6 +52,7 @@ private: COL_TOTAL, COL_AVERAGE, COL_LAST, + COL_MAX, COL_NUM_SAMPLES }; diff --git a/sdrgui/gui/profiledialog.ui b/sdrgui/gui/profiledialog.ui index 030ab6ccb..defb724a3 100644 --- a/sdrgui/gui/profiledialog.ui +++ b/sdrgui/gui/profiledialog.ui @@ -24,7 +24,41 @@ - + + + + + + + Runtime + + + + + + + Elapsed run-time since reset button pressed + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + @@ -59,6 +93,14 @@ Time for last execution of the code + + + Max + + + Maximum length of time taken to execute the code + + Samples @@ -74,10 +116,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Close|QDialogButtonBox::Reset + QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::Reset diff --git a/sdrgui/gui/scaleengine.cpp b/sdrgui/gui/scaleengine.cpp index 1b1709c05..28e0135c3 100644 --- a/sdrgui/gui/scaleengine.cpp +++ b/sdrgui/gui/scaleengine.cpp @@ -34,7 +34,10 @@ static double trunc(double d) QString ScaleEngine::formatTick(double value, int decimalPlaces) { - if (m_physicalUnit != Unit::TimeHMS) + if (m_tickFormatter) { + return m_tickFormatter->formatTick(value); + } + else if (m_physicalUnit != Unit::TimeHMS) { if (m_physicalUnit == Unit::Scientific) { return QString("%1").arg(m_makeOpposite ? -value : value, 0, 'e', m_fixedDecimalPlaces); @@ -862,7 +865,8 @@ ScaleEngine::ScaleEngine() : m_makeOpposite(false), m_truncateMode(false), m_truncated(false), - m_truncationValue(0.0) + m_truncationValue(0.0), + m_tickFormatter(nullptr) { } diff --git a/sdrgui/gui/scaleengine.h b/sdrgui/gui/scaleengine.h index 2ca92f38f..9a57d35d0 100644 --- a/sdrgui/gui/scaleengine.h +++ b/sdrgui/gui/scaleengine.h @@ -39,6 +39,11 @@ public: }; typedef QList TickList; + class TickFormatter { + public: + virtual QString formatTick(double value) const = 0; + }; + ScaleEngine(); void setOrientation(Qt::Orientation orientation); @@ -59,6 +64,9 @@ public: float getScaleWidth(); + void setTickFormatter(TickFormatter *tickFormatter) { m_tickFormatter = tickFormatter; } + void requestReCalc() { m_recalc = true; } + private: // base configuration Qt::Orientation m_orientation; @@ -87,6 +95,8 @@ private: bool m_truncated; //!< true if upper digits are truncated double m_truncationValue; //!< value to subreact from tick display values + TickFormatter *m_tickFormatter; + QString formatTick(double value, int decimalPlaces); void calcCharSize(); void calcScaleFactor(); diff --git a/sdrgui/gui/spectrum.md b/sdrgui/gui/spectrum.md index 018a2e9e8..035f45573 100644 --- a/sdrgui/gui/spectrum.md +++ b/sdrgui/gui/spectrum.md @@ -1,4 +1,4 @@ -

Spectrum component

+

Spectrum component

This page details the spectrum component that takes part of the main spectrum display and is also used in some channel and feature plugins. @@ -28,6 +28,9 @@ A status line is displayed at the left of the top margin. It displays the follow - if frequency zooming is active the zooming factor - `CF:` followed by the Center Frequency of the displayed spectrum possibly with multiplier suffix (G, M, k) - `SP:` followed by the frequency SPan of the displayed spectrum possibly with multiplier suffix (M, k) + - `RBW:` followed by the RBW (Resolution BandWidth) of the displayed spectrum possibly with multiplier suffix (k). This is optional and must be enabled in the Spectrum Display Settings dialog. + - `Cur:` followed by the power in dB and frequency under the cursor. This is optional and must be enabled in the Spectrum Display Settings dialog. + - `Pk:` followed by the power in dB and frequency of the highest peak in the displayed spectrum. This is optional and must be enabled in the Spectrum Display Settings dialog.

Spectrum markers

@@ -93,9 +96,11 @@ When the mouse is inside the time scale (waterfall) the overlap is increased by

B. Spectrum controls

-Controls are organized in 6 blocks arranged in a flow layout so that the size of the control area can adapt to the width of the spectrum arranging the blocks from 4 to 1 line as the spectrum widens. The buttons and various controls in each block remain at the same place. +Not all controls are visible by default. See B.7.8 for how to select which controls are visible. -Narrow (4 lines): +Controls are organized in 7 blocks arranged in a flow layout so that the size of the control area can adapt to the width of the spectrum arranging the blocks from 5 to 1 line as the spectrum widens. The buttons and various controls in each block remain at the same place. + +Narrow (5 lines): ![Spectrum GUI](../../doc/img/MainWindow_spectrum_gui_narrow.png) @@ -103,7 +108,7 @@ Wide (1 line): ![Spectrum GUI](../../doc/img/MainWindow_spectrum_gui_wide.png) -The 6 blocks are detailed next: +The 7 blocks are detailed next: ![Spectrum GUI](../../doc/img/MainWindow_spectrum_gui.png) @@ -254,7 +259,8 @@ Use this combo to select which averaging mode is applied: - **Mov**: moving average. This is a sliding average over the amount of samples specified next (B.2.5). There is one complete FFT line produced at every FFT sampling period - **Fix**: fixed average. Average is done over the amount of samples specified next (B.2.5) and a result is produced at the end of the corresponding period then the next block of averaged samples is processed. There is one complete FFT line produced every FFT sampling period multiplied by the number of averaged samples (4.6). The time scale on the waterfall display is updated accordingly. - **Max**: this is not an averaging but a max hold. It will retain the maximum value over the amount of samples specified next (B.2.5). Similarly to the fixed average a result is produced at the end of the corresponding period which results in slowing down the waterfall display. The point of this mode is to make outlying short bursts within the "averaging" period stand out. With averaging they would only cause a modest increase and could be missed out. - + - **Min**: It will retain the minimum value over the amount of samples specified next (B.2.5). Similarly to the fixed average a result is produced at the end of the corresponding period which results in slowing down the waterfall display. This mode is useful to measure the noise floor by retaining the minimum value over the averaging period. +

B.4.5: Number of averaged samples

Each FFT bin (squared magnitude) is averaged or max'ed over a number of samples. This combo allows selecting the number of samples between these values: 1 (no averaging), 2, 5, 10, 20, 50, 100, 200, 500, 1k (1000) for all modes and in addition 2k, 5k, 10k, 20k, 50k, 1e5 (100000), 2e5, 5e5, 1M (1000000) for "fixed" and "max" modes. Averaging reduces the noise variance and can be used to better detect weak continuous signals. The fixed averaging mode allows long time monitoring on the waterfall. The max mode helps showing short bursts that may appear during the "averaging" period. @@ -263,6 +269,26 @@ The resulting spectrum refresh period appears in the tooltip taking sample rate, Period = ((((FFT_size ÷ 2) - overlap) × 2) ÷ sample_rate) × averaging_size +

B.4.6:Math mode

+ +Use this combo to select which mathematical operation is applied to the spectrum: + - **No**: no math operation. + - **x-μ**: difference between current spectrum and a moving average. + - **x-μ dB**: difference between current spectrum and a moving average after values are converted to dB (so x/μ). + - **x-μ+∧μ dB**: difference between current spectrum and a moving average after values are converted to dB (so x/μ) and then adding the average back in dB (∧μ) to keep the overall level of the spectrum. This is useful to make outlying short bursts within the "averaging" period stand out while keeping the overall level of the spectrum unchanged. With "x-μ dB" the resulting spectrum would be centered around zero which may not be desirable. + - **x-μ dB**: absolute value of difference between current spectrum and a moving average after values are converted to dB (so |x/μ|). + - **x-M1**: difference between current spectrum and the spectrum stored in memory M1. + - **x-M1 dB**: difference between current spectrum and the spectrum stored in memory M1 after values are converted to dB (so x/M1). + - **|x-M1| dB**: absolute value of difference between current spectrum and the spectrum stored in memory M1 after values are converted to dB (so |x/M1|). + - **x-M2**: difference between current spectrum and the spectrum stored in memory M2. + - **x-M2 dB**: difference between current spectrum and the spectrum stored in memory M2 after values are converted to dB (so x/M1). + - **|x-M2| dB**: absolute value of difference between current spectrum and the spectrum stored in memory M1 after values are converted to dB (so |x/M1|). + +

B.4.7: Math moving average length

+ +When the math mode (B.4.6) is set to a mode that requires a moving average (x-μ, x-μ dB, x-μ+∧μ dB, |x-μ| dB) this combo allows selecting the number of samples for the moving average. +One of the predefined values can be selected or a user-defined value can be entered between 2 and 1M. +

B.5: Spectrum display controls - block #5

![Spectrum GUI E](../../doc/img/MainWindow_spectrum_gui_E.png) @@ -298,17 +324,56 @@ Use this toggle button to switch between spectrum logarithmic and linear scale d When in linear mode the range control (B.3.3) has no effect because the actual range is between 0 and the reference level. The reference level in dB (B.3.2) still applies but is translated to a linear value e.g -40 dB is 1e-4. In linear mode the scale numbers are formatted using scientific notation so that they always occupy the same space. -

B.6: Spectrum miscellaneous controls - block #6

+

B.6: Spectrum memory controls - block #6

-![Spectrum GUI F](../../doc/img/MainWindow_spectrum_gui_F.png) +![Spectrum GUI E](../../doc/img/MainWindow_spectrum_gui_F.png) -

B.6.1: Play/Pause spectrum

+ +

B.6.1: Memory 1

+ +Left clicking M1 toggles display of the spectrum stored in memory M1. + +Right clicking M1 displays a menu with the following options: + - **Clear M1**: clears the spectrum stored in memory M1. + - **Set M1 to current spectrum**: stores the current spectrum in memory M1. + - **Set M1 to moving average**: stores the moving average in memory M1. This option is only available when the Math mode (B.4.6) is set to a mode that uses the moving average. + - **Add offset to M1**: displays a dialog that allows the user to enter a value that will be added to each value in M1. This allows the spectrum to be moved up or down. + - **Smooth M1**: smooths the values in M1. Values are set to the average of the neighbouring 5 samples. This can be applied repeatedly to further smooth the spectrum. + - **Set M1 to M1+M2**: adds M1 to M2, storing the result in M1. + - **Set M1 to M1-M2**: subtracts M2 from M1, storing the result in M1. + - **Load M1 from .csv**: loads M1 from a .csv file. The .csv should have a single column named Power, with FFT size rows. + - **Save M1 to .csv**: saves M1 to a .csv file. + +Colour and label for the M1 spectrum can be set in the Spectrum Display Settings dialog. + +

B.6.2: Memory 2

+ +Left clicking M2 toggles display of the spectrum stored in memory M2. + +Right clicking M2 display a menu as described above for M1. + +

B.6.3: Load spectrum to CSV file

+ +Click to specify the name of a .csv file that will be loaded to the current spectrum or spectrum scroll buffer. + +

B.6.4: Save spectrum to CSV file

+ +Click to specify the name of a .csv file that will have the current spectrum or spectrum scroll buffer saved to. + +

B.6.5: Save spectrum/waterfall to image file

+ +Click to specify the name of a .png or .jpg file to save the current display (spectrum and waterfall) to. + +

B.7: Spectrum miscellaneous controls - block #7

+ +![Spectrum GUI F](../../doc/img/MainWindow_spectrum_gui_G.png) + + +

B.7.1: Play/Pause spectrum

Use this button to freeze the spectrum update. Useful when making measurements with the markers. -

B.6.2: Save spectrum to CSV file

- -

B.6.3: Spectrum server control

+

B.7.2: Spectrum server control

A websockets based server can be used to send spectrum data to clients. An example of such client can be found in the [SDRangelSpectrum](https://github.com/f4exb/sdrangelspectrum) project. @@ -368,24 +433,104 @@ The server only sends data. Control including FFT details is done via the REST A -

B.6.4: Spectrum markers dialog

+

B.7.3: Spectrum markers dialog

Opens the [spectrum markers dialog](spectrummarkers.md) -

B.6.5: Spectrum measurements dialog

+

B.7.4: Spectrum measurements dialog

Opens the [Spectrum measurement control dialog](spectrummeasurements.md) Check this link for details on the available measurements. -

B.6.6: Spectrum calibration

+

B.7.5: Spectrum calibration

Use the toggle button to switch between relative and calibrated power readings. Right click to open the [calibration management dialog](spectrumcalibration.md) -

B.6.7: Go to annotation marker

+

B.7.6: Spectrum Display Settings dialog

+ +Click to open the Spectrum Display Settings dialog. See below. + +

B.7.7: Spectrum controls display

+ +Selects which of these spectrum controls are displayed. This allows a user to hide less frequently used controls. + + - **Min**: displays a minimum set of controls. + - **Std**: displays the standard set of controls. + - **All**: displays all controls. + +

B.7.8: Go to annotation marker

This combo only appears if the spectrum display is the spectrum of a device (i.e. main spectrum) and if there are visible annotation markers. It allows to set the device center frequency to the frequency of the selected annotation marker. +

C. Spectrum Display Settings dialog

+ +The Spectrum Display Settings dialog contains settings that do not have dedicated controls below the spectrum. + +![Spectrum Display Settings dialog](../../doc/img/Spectrum_Display_Settings.png) + +

C.1: Waterfall Scrolling

+ +When enabled, a scroll bar will be displayed on the right hand side of the spectrum and spectra will be stored in memory that can be larger than the displayed waterfall. +The scroll bar can then be used to scroll through and display any spectra in memory. The number of spectra that can be stored in memory can be set via the Length (Spectra) field. +The amount of RAM required and total time duration of all spectra are displayed underneath. + +

C.2: Waterfall Axis

+ +

C.2.1: Time units

+ +Specifies what units will be used for the vertical time axis for the waterfall. + + - **Time offset**: displays a time offset. + - **Local time**: displays local time. Only available when Waterfall Scrolling is enabled. + - **UTC time**: displays UTC time. Only available when Waterfall Scrolling is enabled. + +

C.2.2: Time format

+ +When local time or UTC time is used for the time axis, the Time format field species how that time will be formatted. The default is hh:mm:ss. Other examples include: + + - **dd.MM.yyyy** - 21.05.2001 + - **ddd MMMM d yy** - Tue May 21 01 + - **hh:mm:ss.zzz** - 14:13:09.120 + - **hh:mm:ss.z** - 14:13:09.12 + - **h:m:s ap** - 2:13:9 pm + +

C.3: Status Line

+ +Allows customizing which information is displayed in the status line. + +

C.3.1: Display RBW

+ +When checked, the Resolution Bandwidth (RBW) will be displayed in the status line. + +

C.3.2: Display power/frequency under cursor

+ +When checked, the power and frequency of the FFT bin under the cursor will be displayed in the status line. + +

C.3.3: Display peak power/frequency

+ +When checked, the power and frequency of the highest peak will be displayed in the status line. + +

C.4: Spectrum

+ +

C.4.1: Colour

+ +Specifies the color to draw the spectrum when line (B.2.1) or fill (B.2.2) style is selected. + +

C.5: Spectrum Memories

+ +

C.5.1: Memory

+ +Selects which memory settings will be displayed for (M1 or M2). + +

C.5.2: Label

+ +Specifies a text label that will be displayed to the left hand side of the spectrum held in the memory. + +

C.5.3: Color

+ +Specifies the color to draw the spectrum held in the memory. +

3D Spectrogram Controls

![3D Spectrogram](../../doc/img/MainWindow_3D_spectrogram.png) diff --git a/sdrgui/gui/spectrumdisplaysettingsdialog.cpp b/sdrgui/gui/spectrumdisplaysettingsdialog.cpp new file mode 100644 index 000000000..d780e269b --- /dev/null +++ b/sdrgui/gui/spectrumdisplaysettingsdialog.cpp @@ -0,0 +1,201 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "spectrumdisplaysettingsdialog.h" +#include "glspectrum.h" + +#include "ui_spectrumdisplaysettingsdialog.h" + +static QString rgbToColor(quint32 rgb) +{ + QColor color = QColor::fromRgba(rgb); + return QString("%1,%2,%3").arg(color.red()).arg(color.green()).arg(color.blue()); +} + +static QString backgroundCSS(quint32 rgb) +{ + // Must specify a border, otherwise we end up with a gradient instead of solid background + return QString("QToolButton { background-color: rgb(%1); border: none; }").arg(rgbToColor(rgb)); +} + +SpectrumDisplaySettingsDialog::SpectrumDisplaySettingsDialog(GLSpectrum *glSpectrum, SpectrumSettings *settings, int sampleRate, QWidget *parent) : + QDialog(parent), + ui(new Ui::SpectrumDisplaySettingsDialog), + m_glSpectrum(glSpectrum), + m_settings(settings), + m_sampleRate(sampleRate) +{ + ui->setupUi(this); + + m_spectrumColor = m_settings->m_spectrumColor; + ui->spectrumColor->setStyleSheet(backgroundCSS(m_spectrumColor)); + + for (int i = 0; i < settings->m_spectrumMemory.size(); i++) + { + MemorySettings memorySetting = {settings->m_spectrumMemory[i].m_color, settings->m_spectrumMemory[i].m_label}; + m_memorySettings.append(memorySetting); + } + + ui->waterfallScrollBar->setChecked(m_settings->m_scrollBar); + ui->scrollLength->setValue(m_settings->m_scrollLength); + + ui->waterfallVerticalAxisUnits->setCurrentIndex((int) m_settings->m_waterfallTimeUnits); + ui->waterfallVerticalAxisFormat->setText(m_settings->m_waterfallTimeFormat); + + ui->displayRBW->setChecked(m_settings->m_displayRBW); + ui->displayCursorStats->setChecked(m_settings->m_displayCursorStats); + ui->displayPeakStats->setChecked(m_settings->m_displayPeakStats); + + displaySettings(); +} + +SpectrumDisplaySettingsDialog::~SpectrumDisplaySettingsDialog() +{} + +void SpectrumDisplaySettingsDialog::accept() +{ + m_settings->m_waterfallTimeUnits = (SpectrumSettings::WaterfallTimeUnits) ui->waterfallVerticalAxisUnits->currentIndex(); + m_settings->m_waterfallTimeFormat = ui->waterfallVerticalAxisFormat->text(); + m_settings->m_scrollBar = ui->waterfallScrollBar->isChecked(); + m_settings->m_scrollLength = ui->scrollLength->value(); + m_settings->m_displayRBW = ui->displayRBW->isChecked(); + m_settings->m_displayCursorStats = ui->displayCursorStats->isChecked(); + m_settings->m_displayPeakStats = ui->displayPeakStats->isChecked(); + m_settings->m_spectrumColor = m_spectrumColor; + + for (int i = 0; i < m_settings->m_spectrumMemory.size(); i++) + { + m_settings->m_spectrumMemory[i].m_color = m_memorySettings[i].m_color; + m_settings->m_spectrumMemory[i].m_label = m_memorySettings[i].m_label; + } + + QDialog::accept(); +} + +void SpectrumDisplaySettingsDialog::displaySettings() +{ + bool enabled = ui->waterfallScrollBar->isChecked(); + ui->scrollLengthLabel->setEnabled(enabled); + ui->scrollLength->setEnabled(enabled); + ui->scrollRAMLabel->setEnabled(enabled); + ui->scrollRAM->setEnabled(enabled); + ui->scrollTimeLabel->setEnabled(enabled); + ui->scrollTime->setEnabled(enabled); + + ui->waterfallVerticalAxisUnitsLabel->setEnabled(enabled); + ui->waterfallVerticalAxisUnits->setEnabled(enabled); + + bool formatEnabled = ui->waterfallScrollBar->isChecked() && (ui->waterfallVerticalAxisUnits->currentIndex() != (int)SpectrumSettings::WaterfallTimeUnits::TimeOffset); + if (!formatEnabled) { + ui->waterfallVerticalAxisUnits->setCurrentIndex((int) SpectrumSettings::WaterfallTimeUnits::TimeOffset); + } + ui->waterfallVerticalAxisFormatLabel->setEnabled(formatEnabled); + ui->waterfallVerticalAxisFormat->setEnabled(formatEnabled); + + // Calculate RAM usage + + std::size_t ram = ui->scrollLength->value() * (m_settings->m_fftSize * sizeof(float) + sizeof(Real *) + sizeof(int) + sizeof(qint64) + sizeof(QDateTime)); + + ui->scrollRAM->setText(tr("%1 MB").arg(ram / 1024 / 1024)); + + // Calculate time duration of complete scroll history + + float fftRate = m_sampleRate / (float) m_settings->m_fftSize * (100.0f - m_settings->m_fftOverlap) / 100.0f; + if (m_settings->m_averagingMode == SpectrumSettings::AvgModeFixed) { + fftRate /= m_settings->getAveragingValue(m_settings->m_averagingIndex, m_settings->m_averagingMode); + } + int secs = ui->scrollLength->value() / fftRate; + int hours = secs / 3600; + secs -= hours * 3600; + int minutes = secs / 60; + secs -= minutes * 60; + + ui->scrollTime->setText(QString("%1:%2:%3") + .arg(hours, 2, 10, QChar('0')) + .arg(minutes, 2, 10, QChar('0')) + .arg(secs, 2, 10, QChar('0'))); + + displayMemorySettings(); +} + +void SpectrumDisplaySettingsDialog::displayMemorySettings() +{ + int idx = ui->memIdx->currentIndex(); + ui->memColor->setStyleSheet(backgroundCSS(m_memorySettings[idx].m_color)); + ui->memLabel->setText(m_memorySettings[idx].m_label); +} + +void SpectrumDisplaySettingsDialog::on_waterfallVerticalAxisUnits_currentIndexChanged(int index) +{ + (void) index; + + displaySettings(); +} + +void SpectrumDisplaySettingsDialog::on_waterfallScrollBar_clicked(bool checked) +{ + (void) checked; + + displaySettings(); +} + +void SpectrumDisplaySettingsDialog::on_scrollLength_valueChanged(int value) +{ + (void) value; + + displaySettings(); +} + +void SpectrumDisplaySettingsDialog::on_spectrumColor_clicked(bool checked) +{ + (void) checked; + + QColorDialog dialog(QColor::fromRgba(m_spectrumColor), ui->spectrumColor); + if (dialog.exec() == QDialog::Accepted) + { + QRgb color = dialog.selectedColor().rgba(); + ui->spectrumColor->setStyleSheet(backgroundCSS(color)); + m_spectrumColor = color; + } +} + +void SpectrumDisplaySettingsDialog::on_memIdx_currentIndexChanged(int index) +{ + (void) index; + + displayMemorySettings(); +} + +void SpectrumDisplaySettingsDialog::on_memColor_clicked(bool checked) +{ + (void) checked; + + QColorDialog dialog(QColor::fromRgba(m_memorySettings[ui->memIdx->currentIndex()].m_color), ui->memColor); + if (dialog.exec() == QDialog::Accepted) + { + QRgb color = dialog.selectedColor().rgba(); + ui->memColor->setStyleSheet(backgroundCSS(color)); + m_memorySettings[ui->memIdx->currentIndex()].m_color = color; + } +} + +void SpectrumDisplaySettingsDialog::on_memLabel_editingFinished() +{ + m_memorySettings[ui->memIdx->currentIndex()].m_label = ui->memLabel->text(); +} diff --git a/sdrgui/gui/spectrumdisplaysettingsdialog.h b/sdrgui/gui/spectrumdisplaysettingsdialog.h new file mode 100644 index 000000000..62ef99ef3 --- /dev/null +++ b/sdrgui/gui/spectrumdisplaysettingsdialog.h @@ -0,0 +1,66 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2026 Jon Beniston, M7RCE // +// // +// 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 as 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 V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRBASE_GUI_SPECTRUMDISPLAYSETTINGSDIALOG_H_ +#define SDRBASE_GUI_SPECTRUMDISPLAYSETTINGSDIALOG_H_ + +#include + +#include "dsp/spectrumsettings.h" +#include "export.h" + +namespace Ui { + class SpectrumDisplaySettingsDialog; +} + +class GLSpectrum; + +class SDRGUI_API SpectrumDisplaySettingsDialog : public QDialog { + Q_OBJECT + + struct MemorySettings { + QRgb m_color; + QString m_label; + }; + +public: + explicit SpectrumDisplaySettingsDialog(GLSpectrum *glSpectrum, SpectrumSettings *settings, int sampleRate, QWidget *parent = nullptr); + ~SpectrumDisplaySettingsDialog(); + +private: + void displaySettings(); + void displayMemorySettings(); + + Ui::SpectrumDisplaySettingsDialog *ui; + GLSpectrum *m_glSpectrum; + SpectrumSettings *m_settings; + int m_sampleRate; + QRgb m_spectrumColor; + QList m_memorySettings; + +private slots: + void on_waterfallVerticalAxisUnits_currentIndexChanged(int index); + void on_waterfallScrollBar_clicked(bool checked=false); + void on_scrollLength_valueChanged(int value); + void on_spectrumColor_clicked(bool checked=false); + void on_memIdx_currentIndexChanged(int index); + void on_memColor_clicked(bool checked=false); + void on_memLabel_editingFinished(); + void accept() override; +}; + +#endif // SDRBASE_GUI_SPECTRUMDISPLAYSETTINGSDIALOG_H_ diff --git a/sdrgui/gui/spectrumdisplaysettingsdialog.ui b/sdrgui/gui/spectrumdisplaysettingsdialog.ui new file mode 100644 index 000000000..64d9e75de --- /dev/null +++ b/sdrgui/gui/spectrumdisplaysettingsdialog.ui @@ -0,0 +1,424 @@ + + + SpectrumDisplaySettingsDialog + + + + 0 + 0 + 420 + 613 + + + + + 400 + 250 + + + + + Liberation Sans + 9 + + + + Spectrum Display Settings + + + + + + + + Waterfall Scrolling + + + + + + + 0 + 0 + + + + Enabled + + + + + + + Whether scrolling of the waterfall / 3D spectrogram is supported + + + + + + + + + + Length (Spectra) + + + + + + + Length of scroll buffer / how many spectra will be stored in RAM + + + 10000 + + + 1000000 + + + 100000 + + + + + + + RAM usage + + + + + + + How much RAM will be used by the scroll buffer + + + 10 M + + + + + + + Time duration + + + + + + + Time duration of scroll buffer in hh:mm:ss + + + 00:00:00 + + + + + + + + + + + + Waterfall Axis + + + + + + + 0 + 0 + + + + Time units + + + + + + + Values for waterfall/3D spectrogram vertical axis. Scolling needs to be enabled to display system time + + + 0 + + + + Time offset + + + + + Local time + + + + + UTC time + + + + + + + + Time format + + + + + + + Format string for time. + +dd.MM.yyyy - 21.05.2001 +ddd MMMM d yy - Tue May 21 01 +hh:mm:ss.zzz - 14:13:09.120 +hh:mm:ss.z - 14:13:09.12 +h:m:s ap - 2:13:9 pm + + + hh:mm:ss + + + + + + + + + + Status Line + + + + + + Display RBW (Resolution Bandwidth) in status line + + + Display RBW + + + + + + + Display power/frequency under cursor in status line + + + Display power/frequency under cursor + + + + + + + Display peak power/frequency + + + + + + + + + + Spectrum + + + + + + + 60 + 0 + + + + Color + + + + + + + Spectrum color + + + + + + + + + + + + + Spectrum Memories + + + + + + Memory + + + + + + + + + Select which memory to display settings for + + + + M1 + + + + + M2 + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + Label + + + + + + + + + + + 60 + 0 + + + + Color + + + + + + + + + Memory color + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + + + buttonBox + accepted() + SpectrumDisplaySettingsDialog + accept() + + + 257 + 194 + + + 157 + 203 + + + + + buttonBox + rejected() + SpectrumDisplaySettingsDialog + reject() + + + 314 + 194 + + + 286 + 203 + + + + + diff --git a/sdrgui/gui/spectrummeasurements.cpp b/sdrgui/gui/spectrummeasurements.cpp index 3ee45f2d6..0458cc670 100644 --- a/sdrgui/gui/spectrummeasurements.cpp +++ b/sdrgui/gui/spectrummeasurements.cpp @@ -198,7 +198,8 @@ SpectrumMeasurements::SpectrumMeasurements(QWidget *parent) : m_measurement(SpectrumSettings::MeasurementPeaks), m_precision(1), m_table(nullptr), - m_peakTable(nullptr) + m_peakTable(nullptr), + m_maskTable(nullptr) { m_textBrush.setColor(Qt::white); // Should get this from the style sheet? m_redBrush.setColor(Qt::red); @@ -263,6 +264,10 @@ void SpectrumMeasurements::createMeasurementsTable(const QStringList &rows, cons // Cell context menu m_table->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_table, &QTableWidget::customContextMenuRequested, this, &SpectrumMeasurements::tableContextMenu); + + // Enable mouse tracking for FramelessWindowResizer, as table is created after FramelessWindowResizer::enableChildMouseTracking is called + m_table->setMouseTracking(true); + m_table->viewport()->setMouseTracking(true); } void SpectrumMeasurements::createPeakTable(int peaks) @@ -306,6 +311,10 @@ void SpectrumMeasurements::createPeakTable(int peaks) // Cell context menu m_peakTable->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_peakTable, &QTableWidget::customContextMenuRequested, this, &SpectrumMeasurements::peakTableContextMenu); + + // Enable mouse tracking for FramelessWindowResizer, as table is created after FramelessWindowResizer::enableChildMouseTracking is called + m_peakTable->setMouseTracking(true); + m_peakTable->viewport()->setMouseTracking(true); } void SpectrumMeasurements::createTableMenus() @@ -371,6 +380,52 @@ void SpectrumMeasurements::createSNRTable() createMeasurementsTable(rows, units); } +void SpectrumMeasurements::createMaskTable(unsigned memMask) +{ + int memories = qPopulationCount(memMask); + + m_maskTable = new SpectrumMeasurementsTable(); + m_maskTable->horizontalHeader()->setSectionsMovable(true); + + QStringList columns = QStringList{"Tests", "Fails", ""}; + + m_maskTable->setColumnCount(columns.size()); + m_maskTable->setRowCount(memories); + + for (int i = 0; i < columns.size(); i++) { + m_maskTable->setHorizontalHeaderItem(i, new QTableWidgetItem(columns[i])); + } + m_maskTable->horizontalHeader()->setStretchLastSection(true); + + int row = 0; + for (int i = 0; i < 32; i++) + { + if ((memMask & (1 << i)) != 0) + { + m_maskTable->setVerticalHeaderItem(row, new QTableWidgetItem(QString("M%1").arg(i + 1))); + + for (int j = 0; j < 2; j++) + { + QTableWidgetItem *item = new QTableWidgetItem(); + item->setFlags(Qt::ItemIsEnabled); + m_maskTable->setItem(row, j, item); + } + row++; + } + } + + m_maskTable->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + m_maskTable->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + + // Cell context menu + m_maskTable->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_maskTable, &QTableWidget::customContextMenuRequested, this, &SpectrumMeasurements::maskTableContextMenu); + + // Enable mouse tracking for FramelessWindowResizer, as table is created after FramelessWindowResizer::enableChildMouseTracking is called + m_maskTable->setMouseTracking(true); + m_maskTable->viewport()->setMouseTracking(true); +} + // Create column select menu item QAction *SpectrumMeasurements::createCheckableItem(QString &text, int idx, bool checked, bool row) { @@ -458,12 +513,34 @@ void SpectrumMeasurements::peakTableContextMenu(QPoint pos) connect(copyAction, &QAction::triggered, this, [text]()->void { QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setText(text); - }); + }); tableContextMenu->addAction(copyAction); tableContextMenu->addSeparator(); tableContextMenu->popup(m_peakTable->viewport()->mapToGlobal(pos)); - } + } +} + +void SpectrumMeasurements::maskTableContextMenu(QPoint pos) +{ + QTableWidgetItem *item = m_maskTable->itemAt(pos); + if (item) + { + QMenu* tableContextMenu = new QMenu(m_maskTable); + connect(tableContextMenu, &QMenu::aboutToHide, tableContextMenu, &QMenu::deleteLater); + + // Copy current cell + QAction* copyAction = new QAction("Copy", tableContextMenu); + const QString text = item->text(); + connect(copyAction, &QAction::triggered, this, [text]()->void { + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(text); + }); + tableContextMenu->addAction(copyAction); + tableContextMenu->addSeparator(); + + tableContextMenu->popup(m_peakTable->viewport()->mapToGlobal(pos)); + } } void SpectrumMeasurements::resizeMeasurementsTable() @@ -495,10 +572,11 @@ void SpectrumMeasurements::resizePeakTable() m_peakTable->removeRow(row); } -void SpectrumMeasurements::setMeasurementParams(SpectrumSettings::Measurement measurement, int peaks, int precision) +void SpectrumMeasurements::setMeasurementParams(SpectrumSettings::Measurement measurement, int peaks, int precision, unsigned memMasks) { if ( (measurement != m_measurement) || (m_precision != precision) + || (m_memMask != memMasks) || ((m_peakTable == nullptr) && (m_table == nullptr)) || ((m_peakTable != nullptr) && (peaks != m_peakTable->rowCount())) ) @@ -508,9 +586,12 @@ void SpectrumMeasurements::setMeasurementParams(SpectrumSettings::Measurement me m_peakTable = nullptr; delete m_table; m_table = nullptr; + delete m_maskTable; + m_maskTable = nullptr; m_measurement = measurement; m_precision = precision; + m_memMask = memMasks; switch (measurement) { @@ -543,6 +624,11 @@ void SpectrumMeasurements::setMeasurementParams(SpectrumSettings::Measurement me createSNRTable(); layout()->addWidget(m_table); break; + case SpectrumSettings::MeasurementMask: + reset(); + createMaskTable(m_memMask); + layout()->addWidget(m_maskTable); + break; default: break; } @@ -558,6 +644,11 @@ void SpectrumMeasurements::setMeasurementParams(SpectrumSettings::Measurement me m_table->show(); resize(sizeHint()); } + else if (m_maskTable) + { + m_maskTable->show(); + resize(sizeHint()); + } } } @@ -578,6 +669,16 @@ void SpectrumMeasurements::reset() } } } + if (m_maskTable) + { + for (int i = 0; i < m_maskTable->rowCount(); i++) + { + for (int j = COL_MASK_TESTS; j <= COL_MASK_FAILS; j++) + { + m_maskTable->item(i, j)->setData(Qt::DisplayRole, 0); + } + } + } } // Check the value meets the user-defined specification @@ -614,74 +715,88 @@ bool SpectrumMeasurements::checkSpec(const QString &spec, double value) const return false; } -void SpectrumMeasurements::updateMeasurement(int row, float value) +void SpectrumMeasurements::updateMeasurement(int row, float value, bool updateGUI) { - m_measurements[row].add(value); - double mean = m_measurements[row].mean(); - - m_table->item(row, COL_CURRENT)->setData(Qt::DisplayRole, value); - m_table->item(row, COL_MEAN)->setData(Qt::DisplayRole, mean); - m_table->item(row, COL_MIN)->setData(Qt::DisplayRole, m_measurements[row].m_min); - m_table->item(row, COL_MAX)->setData(Qt::DisplayRole, m_measurements[row].m_max); - m_table->item(row, COL_RANGE)->setData(Qt::DisplayRole, m_measurements[row].m_max - m_measurements[row].m_min); - m_table->item(row, COL_STD_DEV)->setData(Qt::DisplayRole, m_measurements[row].stdDev()); - m_table->item(row, COL_COUNT)->setData(Qt::DisplayRole, m_measurements[row].m_values.size()); - - QString spec = m_table->item(row, COL_SPEC)->text(); - bool valueOK = checkSpec(spec, value); - bool meanOK = checkSpec(spec, mean); - bool minOK = checkSpec(spec, m_measurements[row].m_min); - bool mmaxOK = checkSpec(spec, m_measurements[row].m_max); - - if (!valueOK) + if (!updateGUI) { - m_measurements[row].m_fails++; - m_table->item(row, 8)->setData(Qt::DisplayRole, m_measurements[row].m_fails); + m_measurements[row].add(value); + + double mean = m_measurements[row].mean(); + + QString spec = m_table->item(row, COL_SPEC)->text(); + bool valueOK = checkSpec(spec, value); + checkSpec(spec, mean); + checkSpec(spec, m_measurements[row].m_min); + checkSpec(spec, m_measurements[row].m_max); + + if (!valueOK) { + m_measurements[row].m_fails++; + } } + else + { + double mean = m_measurements[row].mean(); - // item->setForeground doesn't work, perhaps as we have style sheet applied? - m_table->item(row, COL_CURRENT)->setData(Qt::ForegroundRole, valueOK ? m_textBrush : m_redBrush); - m_table->item(row, COL_MEAN)->setData(Qt::ForegroundRole, meanOK ? m_textBrush : m_redBrush); - m_table->item(row, COL_MIN)->setData(Qt::ForegroundRole, minOK ? m_textBrush : m_redBrush); - m_table->item(row, COL_MAX)->setData(Qt::ForegroundRole, mmaxOK ? m_textBrush : m_redBrush); + m_table->item(row, COL_CURRENT)->setData(Qt::DisplayRole, value); + m_table->item(row, COL_MEAN)->setData(Qt::DisplayRole, mean); + m_table->item(row, COL_MIN)->setData(Qt::DisplayRole, m_measurements[row].m_min); + m_table->item(row, COL_MAX)->setData(Qt::DisplayRole, m_measurements[row].m_max); + m_table->item(row, COL_RANGE)->setData(Qt::DisplayRole, m_measurements[row].m_max - m_measurements[row].m_min); + m_table->item(row, COL_STD_DEV)->setData(Qt::DisplayRole, m_measurements[row].stdDev()); + m_table->item(row, COL_COUNT)->setData(Qt::DisplayRole, m_measurements[row].m_values.size()); + + QString spec = m_table->item(row, COL_SPEC)->text(); + bool valueOK = checkSpec(spec, value); + bool meanOK = checkSpec(spec, mean); + bool minOK = checkSpec(spec, m_measurements[row].m_min); + bool mmaxOK = checkSpec(spec, m_measurements[row].m_max); + + m_table->item(row, 8)->setData(Qt::DisplayRole, m_measurements[row].m_fails); + + // item->setForeground doesn't work, perhaps as we have style sheet applied? + m_table->item(row, COL_CURRENT)->setData(Qt::ForegroundRole, valueOK ? m_textBrush : m_redBrush); + m_table->item(row, COL_MEAN)->setData(Qt::ForegroundRole, meanOK ? m_textBrush : m_redBrush); + m_table->item(row, COL_MIN)->setData(Qt::ForegroundRole, minOK ? m_textBrush : m_redBrush); + m_table->item(row, COL_MAX)->setData(Qt::ForegroundRole, mmaxOK ? m_textBrush : m_redBrush); + } } -void SpectrumMeasurements::setSNR(float snr, float snfr, float thd, float thdpn, float sinad) +void SpectrumMeasurements::setSNR(float snr, float snfr, float thd, float thdpn, float sinad, bool updateGUI) { - updateMeasurement(0, snr); - updateMeasurement(1, snfr); - updateMeasurement(2, thd); - updateMeasurement(3, thdpn); - updateMeasurement(4, sinad); + updateMeasurement(0, snr, updateGUI); + updateMeasurement(1, snfr, updateGUI); + updateMeasurement(2, thd, updateGUI); + updateMeasurement(3, thdpn, updateGUI); + updateMeasurement(4, sinad, updateGUI); } -void SpectrumMeasurements::setSFDR(float sfdr) +void SpectrumMeasurements::setSFDR(float sfdr, bool updateGUI) { - updateMeasurement(5, sfdr); + updateMeasurement(5, sfdr, updateGUI); } -void SpectrumMeasurements::setChannelPower(float power) +void SpectrumMeasurements::setChannelPower(float power, bool updateGUI) { - updateMeasurement(0, power); + updateMeasurement(0, power, updateGUI); } -void SpectrumMeasurements::setAdjacentChannelPower(float left, float leftACPR, float center, float right, float rightACPR) +void SpectrumMeasurements::setAdjacentChannelPower(float left, float leftACPR, float center, float right, float rightACPR, bool updateGUI) { - updateMeasurement(0, left); - updateMeasurement(1, leftACPR); - updateMeasurement(2, center); - updateMeasurement(3, right); - updateMeasurement(4, rightACPR); + updateMeasurement(0, left, updateGUI); + updateMeasurement(1, leftACPR, updateGUI); + updateMeasurement(2, center, updateGUI); + updateMeasurement(3, right, updateGUI); + updateMeasurement(4, rightACPR, updateGUI); } -void SpectrumMeasurements::setOccupiedBandwidth(float occupiedBandwidth) +void SpectrumMeasurements::setOccupiedBandwidth(float occupiedBandwidth, bool updateGUI) { - updateMeasurement(0, occupiedBandwidth); + updateMeasurement(0, occupiedBandwidth, updateGUI); } -void SpectrumMeasurements::set3dBBandwidth(float bandwidth) +void SpectrumMeasurements::set3dBBandwidth(float bandwidth, bool updateGUI) { - updateMeasurement(0, bandwidth); + updateMeasurement(0, bandwidth, updateGUI); } void SpectrumMeasurements::setPeak(int peak, int64_t frequency, float power) @@ -696,3 +811,28 @@ void SpectrumMeasurements::setPeak(int peak, int64_t frequency, float power) qDebug() << "SpectrumMeasurements::setPeak: Attempt to set peak " << peak << " when only " << m_peakTable->rowCount() << " rows in peak table"; } } + +void SpectrumMeasurements::setMaskTestResult(int memoryIdx, qint64 count, qint64 fails) +{ + int row = 0; + + for (int i = 0; i < memoryIdx; i++) + { + if (m_memMask & (1 << i)) { + row++; + } + } + + if (row < m_maskTable->rowCount()) + { + QTableWidgetItem *testsItem = m_maskTable->item(row, COL_MASK_TESTS); + QTableWidgetItem *failsItem = m_maskTable->item(row, COL_MASK_FAILS); + + testsItem->setData(Qt::DisplayRole, count); + failsItem->setData(Qt::DisplayRole, fails); + } + else + { + qDebug() << "SpectrumMeasurements::addMaskTestResult: Result for memory " << memoryIdx << " when only " << m_maskTable->rowCount() << " rows in mask table"; + } +} diff --git a/sdrgui/gui/spectrummeasurements.h b/sdrgui/gui/spectrummeasurements.h index 96c9ec0fc..c14955e66 100644 --- a/sdrgui/gui/spectrummeasurements.h +++ b/sdrgui/gui/spectrummeasurements.h @@ -91,14 +91,15 @@ class SDRGUI_API SpectrumMeasurements : public QWidget { public: SpectrumMeasurements(QWidget *parent = nullptr); - void setMeasurementParams(SpectrumSettings::Measurement measurement, int peaks, int precision); - void setSNR(float snr, float snfr, float thd, float thdpn, float sinad); - void setSFDR(float sfdr); - void setChannelPower(float power); - void setAdjacentChannelPower(float left, float leftACPR, float center, float right, float rightACPR); - void setOccupiedBandwidth(float occupiedBandwidth); - void set3dBBandwidth(float bandwidth); + void setMeasurementParams(SpectrumSettings::Measurement measurement, int peaks, int precision, unsigned memMasks); + void setSNR(float snr, float snfr, float thd, float thdpn, float sinad, bool updateGUI); + void setSFDR(float sfdr, bool updateGUI); + void setChannelPower(float power, bool updateGUI); + void setAdjacentChannelPower(float left, float leftACPR, float center, float right, float rightACPR, bool updateGUI); + void setOccupiedBandwidth(float occupiedBandwidth, bool updateGUI); + void set3dBBandwidth(float bandwidth, bool updateGUI); void setPeak(int peak, int64_t frequency, float power); + void setMaskTestResult(int memoryIdx, qint64 count, qint64 fails); void reset(); private: @@ -110,8 +111,10 @@ private: void createOccupiedBandwidthTable(); void create3dBBandwidthTable(); void createSNRTable(); + void createMaskTable(unsigned memMask); void tableContextMenu(QPoint pos); void peakTableContextMenu(QPoint pos); + void maskTableContextMenu(QPoint pos); void rowSelectMenu(QPoint pos); void rowSelectMenuChecked(bool checked); void columnSelectMenu(QPoint pos); @@ -119,11 +122,12 @@ private: QAction *createCheckableItem(QString &text, int idx, bool checked, bool row); void resizeMeasurementsTable(); void resizePeakTable(); - void updateMeasurement(int row, float value); + void updateMeasurement(int row, float value, bool updateGUI); bool checkSpec(const QString &spec, double value) const; SpectrumSettings::Measurement m_measurement; int m_precision; + unsigned m_memMask; SpectrumMeasurementsTable *m_table; QMenu *m_rowMenu; @@ -134,6 +138,8 @@ private: QBrush m_textBrush; QBrush m_redBrush; + SpectrumMeasurementsTable *m_maskTable; + enum MeasurementsCol { COL_CURRENT, COL_MEAN, @@ -153,6 +159,11 @@ private: COL_PEAK_EMPTY }; + enum MaskTableCol { + COL_MASK_TESTS, + COL_MASK_FAILS + }; + static const QStringList m_measurementColumns; static const QStringList m_tooltips; diff --git a/sdrgui/gui/spectrummeasurements.md b/sdrgui/gui/spectrummeasurements.md index 87d64da62..0dc3d0236 100644 --- a/sdrgui/gui/spectrummeasurements.md +++ b/sdrgui/gui/spectrummeasurements.md @@ -116,6 +116,13 @@ SINAD is measured as per SNR, but the result is the ratio of the fundamental to SFDR is a measurement of the difference in power from the largest peak (the fundamental) to the second largest peak (the strongest spurious signal). +

Mask Test

+ +The mask test measurement checks whether the spectrum exceeds a user-defined mask. The mask is defined by a set of frequency and power points, held in memory M1 or M2. +Any points where the spectrum exceeds the mask are highlighted in red and held. The measurement results table shows the number of spectrum that fail the mask test. + +![Spectrum Measurements - Mask Test](../../doc/img/Spectrum_Measurement_Mask_Test.png) +

Specifications

The measurements table has a Spec column that allows entry of user-defined specifications for the Current, Mean, Min and Max values to be checked against. diff --git a/sdrgui/gui/spectrummeasurementsdialog.cpp b/sdrgui/gui/spectrummeasurementsdialog.cpp index b23241761..78be30f76 100644 --- a/sdrgui/gui/spectrummeasurementsdialog.cpp +++ b/sdrgui/gui/spectrummeasurementsdialog.cpp @@ -57,6 +57,9 @@ SpectrumMeasurementsDialog::SpectrumMeasurementsDialog(GLSpectrum *glSpectrum, S ui->harmonics->setValue(m_settings->m_measurementHarmonics); ui->peaks->setValue(m_settings->m_measurementPeaks); + ui->m1Mask->setChecked(m_settings->m_measurementMemMasks & 1); + ui->m2Mask->setChecked(m_settings->m_measurementMemMasks & 2); + displaySettings(); } @@ -66,11 +69,12 @@ SpectrumMeasurementsDialog::~SpectrumMeasurementsDialog() void SpectrumMeasurementsDialog::displaySettings() { bool show = m_settings->m_measurement != SpectrumSettings::MeasurementNone; + bool mask = m_settings->m_measurement == SpectrumSettings::MeasurementMask; ui->positionLabel->setVisible(show); ui->position->setVisible(show); - ui->precisionLabel->setVisible(show); - ui->precision->setVisible(show); + ui->precisionLabel->setVisible(show && !mask); + ui->precision->setVisible(show && !mask); ui->highlightLabel->setVisible(show); ui->highlight->setVisible(show); @@ -98,8 +102,11 @@ void SpectrumMeasurementsDialog::displaySettings() bool peaks = (m_settings->m_measurement == SpectrumSettings::MeasurementPeaks); ui->peaksLabel->setVisible(peaks && show); ui->peaks->setVisible(peaks && show); -} + ui->maskLabel->setVisible(mask); + ui->m1Mask->setVisible(mask); + ui->m2Mask->setVisible(mask); +} void SpectrumMeasurementsDialog::on_measurement_currentIndexChanged(int index) { @@ -131,7 +138,7 @@ void SpectrumMeasurementsDialog::on_resetMeasurements_clicked(bool checked) (void) checked; if (m_glSpectrum) { - m_glSpectrum->getMeasurements()->reset(); + m_glSpectrum->resetMeasurements(); } } @@ -170,3 +177,17 @@ void SpectrumMeasurementsDialog::on_peaks_valueChanged(int value) m_settings->m_measurementPeaks = value; emit updateMeasurements(); } + +void SpectrumMeasurementsDialog::on_m1Mask_toggled(bool checked) +{ + m_settings->m_measurementMemMasks &= ~1; + m_settings->m_measurementMemMasks |= checked ? 1 : 0; + emit updateMeasurements(); +} + +void SpectrumMeasurementsDialog::on_m2Mask_toggled(bool checked) +{ + m_settings->m_measurementMemMasks &= ~2; + m_settings->m_measurementMemMasks |= checked ? 2 : 0; + emit updateMeasurements(); +} diff --git a/sdrgui/gui/spectrummeasurementsdialog.h b/sdrgui/gui/spectrummeasurementsdialog.h index 6be0e653d..b215c76be 100644 --- a/sdrgui/gui/spectrummeasurementsdialog.h +++ b/sdrgui/gui/spectrummeasurementsdialog.h @@ -58,6 +58,8 @@ private slots: void on_adjChBandwidth_changed(qint64 value); void on_harmonics_valueChanged(int value); void on_peaks_valueChanged(int value); + void on_m1Mask_toggled(bool checked); + void on_m2Mask_toggled(bool checked); signals: void updateMeasurements(); diff --git a/sdrgui/gui/spectrummeasurementsdialog.ui b/sdrgui/gui/spectrummeasurementsdialog.ui index c1dd86019..430ac6861 100644 --- a/sdrgui/gui/spectrummeasurementsdialog.ui +++ b/sdrgui/gui/spectrummeasurementsdialog.ui @@ -7,7 +7,7 @@ 0 0 400 - 302 + 315
@@ -28,105 +28,6 @@ - - - - Highlight on spectrum - - - - - - - Number of harmonics - - - 20 - - - - - - - - 0 - 0 - - - - - 32 - 16 - - - - - DejaVu Sans Mono - 12 - - - - PointingHandCursor - - - Qt::StrongFocus - - - Center frequency offset (Hz) - - - - - - - Channel spacing - - - 2 - - - - - - - Where to display the measurements result table - - - - Above spectrum - - - - - Below spectrum - - - - - Left of spectrum - - - - - Right of spectrum - - - - - - - - Peaks - - - - - - - Center frequency offset - - - @@ -170,119 +71,11 @@ SNR -
-
- - - - Display results table - - - - - - - Channel bandwidth - - - 2 - - - - - - - - 0 - 0 - - - - - 32 - 16 - - - - - DejaVu Sans Mono - 12 - - - - PointingHandCursor - - - Qt::StrongFocus - - - Channel spacing (Hz) - - - - - - - Results precision - - - - - - - - 0 - 0 - - - - - 32 - 16 - - - - - DejaVu Sans Mono - 12 - - - - PointingHandCursor - - - Qt::StrongFocus - - - Channel bandwidth (Hz) - - - - - - - Measurement - - - - - - - Whether to highlight the measurements in the spectrum - - - - - - - - - - Harmonics - - - 2 - + + + Mask test + + @@ -298,26 +91,6 @@ - - - - Adjacent channel bandwidth - - - 2 - - - - - - - Precision of results (number of decimal places) - - - 9 - - - @@ -342,19 +115,301 @@ PointingHandCursor - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus Adjacent channel bandwidth (Hz) + + + + Results precision + + + + + + + Harmonics + + + 2 + + + + + + + Precision of results (number of decimal places) + + + 9 + + + + + + + + + Use memory 2 as mask + + + M1 + + + true + + + + + + + Use memory 1 as mask + + + M2 + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + Number of harmonics + + + 20 + + + + + + + Whether to highlight the measurements in the spectrum + + + + + + + + + + Highlight on spectrum + + + + + + + Peaks + + + + + + + Center frequency offset + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + DejaVu Sans Mono + 12 + + + + PointingHandCursor + + + Qt::FocusPolicy::StrongFocus + + + Center frequency offset (Hz) + + + + + + + Channel bandwidth + + + 2 + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + DejaVu Sans Mono + 12 + + + + PointingHandCursor + + + Qt::FocusPolicy::StrongFocus + + + Channel bandwidth (Hz) + + + + + + + Channel spacing + + + 2 + + + + + + + Measurement + + + + + + + Where to display the measurements result table + + + + Above spectrum + + + + + Below spectrum + + + + + Left of spectrum + + + + + Right of spectrum + + + + + + + + Display results table + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + DejaVu Sans Mono + 12 + + + + PointingHandCursor + + + Qt::FocusPolicy::StrongFocus + + + Channel spacing (Hz) + + + + + + + Memories to use as masks + + + + + + + Adjacent channel bandwidth + + + 2 + + +
- Qt::Vertical + Qt::Orientation::Vertical @@ -379,10 +434,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Close + QDialogButtonBox::StandardButton::Close diff --git a/sdrgui/mainwindow.cpp b/sdrgui/mainwindow.cpp index db81a4eda..6b4c3c508 100644 --- a/sdrgui/mainwindow.cpp +++ b/sdrgui/mainwindow.cpp @@ -3423,7 +3423,7 @@ void MainWindow::startAll() startAllDevices(workspace); } // Start all features - for (int featureSetIndex = 0; featureSetIndex < m_featureUIs.size(); featureSetIndex++) + for (std::size_t featureSetIndex = 0; featureSetIndex < m_featureUIs.size(); featureSetIndex++) { for (int featureIndex = 0; featureIndex < m_featureUIs[featureSetIndex]->getNumberOfFeatures(); featureIndex++) { FeatureWebAPIUtils::run(featureSetIndex, featureIndex);