mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-03-31 12:15:36 -04:00
Added optional scrollbar to be able to scroll back through waterfall.
When scrolling is enabled:
Can adjust power scale for complete waterfall, not just future spectra.
Waterfall time axis can use local time or UTC.
Waterfall not lost when resizing window.
Can now zoom when device is stopped.
Added Min averaging type.
Added button to load spectrum from .csv file.
Add button to save spectrum/waterfall to .png or jpg.
Changed show all controls button to a combobox with choices of Min/Std/All (Minimum/Standard/All).
Changed some buttons in spectrum GUI from QPushButton to QToolButton so their size matches the others.
Fix spectrum from displaying a mixture of old and new spectrums (m_currentSpectrum was a pointer to SpectrumVis buffer).
Added M1 and M2 memories to allow display of reference spectra.
Added math operations to allow spectrum to be difference of current spectrum and either memory or a moving average.
Fixed measurement counts, so they are performed once per spectrum, not on displayed spectra.
Added spectrum mask measurement, to check when a spectrum exceeds mask held in M1 or M2.
Optionally display power/frequency under cursor in status line.
Optionally display peak power/frequency in status line.
Fix incorrect nyquist sample replication, when zoom used.
Fix cursor not changing from resize to pointer when moving over spectrum measurements window.
Add spectrum colour setting.
5902 lines
201 KiB
C++
5902 lines
201 KiB
C++
///////////////////////////////////////////////////////////////////////////////////
|
|
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
|
|
// written by Christian Daniel //
|
|
// Copyright (C) 2014 John Greb <hexameron@spam.no> //
|
|
// Copyright (C) 2015-2023 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
|
|
// Copyright (C) 2015 Hoernchen <la@tfc-server.de> //
|
|
// Copyright (C) 2018 beta-tester <alpha-beta-release@gmx.net> //
|
|
// Copyright (C) 2022-2023 Jon Beniston, M7RCE <jon@beniston.com> //
|
|
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
|
|
// Copyright (C) 2023 Arne Jünemann <das-iro@das-iro.de> //
|
|
// //
|
|
// 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 <http://www.gnu.org/licenses/>. //
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include <algorithm>
|
|
|
|
#include <QtAlgorithms>
|
|
#include <QMouseEvent>
|
|
#include <QOpenGLShaderProgram>
|
|
#include <QOpenGLFunctions>
|
|
#include <QPainter>
|
|
#include <QFontDatabase>
|
|
#include <QWindow>
|
|
#include <QGestureEvent>
|
|
#include <QPanGesture>
|
|
#include <QPinchGesture>
|
|
#include "maincore.h"
|
|
#include "dsp/spectrumvis.h"
|
|
#include "gui/glspectrumview.h"
|
|
#include "gui/spectrummeasurements.h"
|
|
#include "settings/mainsettings.h"
|
|
#include "util/messagequeue.h"
|
|
#include "util/db.h"
|
|
#include "util/profiler.h"
|
|
#include "util/csv.h"
|
|
|
|
#include <QDebug>
|
|
|
|
MESSAGE_CLASS_DEFINITION(GLSpectrumView::MsgReportSampleRate, Message)
|
|
MESSAGE_CLASS_DEFINITION(GLSpectrumView::MsgReportWaterfallShare, Message)
|
|
MESSAGE_CLASS_DEFINITION(GLSpectrumView::MsgReportFFTOverlap, Message)
|
|
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;
|
|
|
|
GLSpectrumView::GLSpectrumView(QWidget* parent) :
|
|
QOpenGLWidget(parent),
|
|
m_markersDisplay(SpectrumSettings::MarkersDisplaySpectrum),
|
|
m_histogramFindPeaks(false),
|
|
m_cursorState(CSNormal),
|
|
m_cursorChannel(0),
|
|
m_spectrumVis(nullptr),
|
|
m_fpsPeriodMs(50),
|
|
m_mouseInside(false),
|
|
m_changesPending(true),
|
|
m_redrawAll(false),
|
|
m_centerFrequency(100000000),
|
|
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),
|
|
m_timingRate(1),
|
|
m_fftOverlap(0),
|
|
m_fftSize(512),
|
|
m_nbBins(512),
|
|
m_fftMin(0),
|
|
m_fftMax(512),
|
|
m_displayGrid(true),
|
|
m_displayGridIntensity(5),
|
|
m_displayTraceIntensity(50),
|
|
m_invertedWaterfall(true),
|
|
m_displayMaxHold(false),
|
|
m_currentSpectrum(nullptr),
|
|
m_displayCurrent(false),
|
|
m_leftMargin(0),
|
|
m_rightMargin(0),
|
|
m_topMargin(0),
|
|
m_frequencyScaleHeight(0),
|
|
m_histogramHeight(80),
|
|
m_waterfallHeight(0),
|
|
m_bottomMargin(0),
|
|
m_waterfallBuffer(nullptr),
|
|
m_waterfallBufferPos(0),
|
|
m_waterfallTextureHeight(-1),
|
|
m_waterfallTexturePos(0),
|
|
m_displayWaterfall(true),
|
|
m_ssbSpectrum(false),
|
|
m_lsbDisplay(false),
|
|
m_3DSpectrogramBuffer(nullptr),
|
|
m_3DSpectrogramBufferPos(0),
|
|
m_3DSpectrogramTextureHeight(-1),
|
|
m_3DSpectrogramTexturePos(0),
|
|
m_display3DSpectrogram(false),
|
|
m_rotate3DSpectrogram(false),
|
|
m_pan3DSpectrogram(false),
|
|
m_scaleZ3DSpectrogram(false),
|
|
m_3DSpectrogramStyle(SpectrumSettings::Outline),
|
|
m_spectrumColor(255, 255, 63),
|
|
m_colorMapName("Angel"),
|
|
m_scrollFrequency(false),
|
|
m_scrollStartCenterFreq(0),
|
|
m_pinching(false),
|
|
m_pinching3D(false),
|
|
m_frequencyRequested(false),
|
|
m_nextFrequencyValid(false),
|
|
m_histogramBuffer(nullptr),
|
|
m_histogram(nullptr),
|
|
m_displayHistogram(true),
|
|
m_displayChanged(false),
|
|
m_displaySourceOrSink(true),
|
|
m_displayStreamIndex(0),
|
|
m_matrixLoc(0),
|
|
m_colorLoc(0),
|
|
m_useCalibration(false),
|
|
m_calibrationGain(1.0),
|
|
m_calibrationShiftdB(0.0),
|
|
m_calibrationInterpMode(SpectrumSettings::CalibInterpLinear),
|
|
m_messageQueueToGUI(nullptr),
|
|
m_openGLLogger(nullptr),
|
|
m_isDeviceSpectrum(false),
|
|
m_measurements(nullptr),
|
|
m_measurement(SpectrumSettings::MeasurementNone),
|
|
m_measurementCenterFrequencyOffset(0),
|
|
m_measurementBandwidth(10000),
|
|
m_measurementChSpacing(10000),
|
|
m_measurementAdjChBandwidth(10000),
|
|
m_measurementHarmonics(5),
|
|
m_measurementPeaks(5),
|
|
m_measurementHighlight(true),
|
|
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();
|
|
if (multisamples > 0)
|
|
{
|
|
QSurfaceFormat format;
|
|
format.setSamples(multisamples);
|
|
setFormat(format);
|
|
}
|
|
|
|
setObjectName("GLSpectrum");
|
|
setAutoFillBackground(false);
|
|
setAttribute(Qt::WA_OpaquePaintEvent, true);
|
|
setAttribute(Qt::WA_NoSystemBackground, true);
|
|
setMouseTracking(true);
|
|
|
|
setMinimumSize(360, 200);
|
|
|
|
m_waterfallShare = 0.5;
|
|
|
|
for (int i = 0; i <= 239; i++)
|
|
{
|
|
QColor c;
|
|
c.setHsv(239 - i, 255, 15 + i);
|
|
((quint8*)&m_waterfallPalette[i])[0] = c.red();
|
|
((quint8*)&m_waterfallPalette[i])[1] = c.green();
|
|
((quint8*)&m_waterfallPalette[i])[2] = c.blue();
|
|
((quint8*)&m_waterfallPalette[i])[3] = c.alpha();
|
|
}
|
|
|
|
m_waterfallPalette[239] = 0xffffffff;
|
|
m_histogramPalette[0] = 0;
|
|
|
|
for (int i = 1; i < 240; i++)
|
|
{
|
|
QColor c;
|
|
int light = i < 60 ? 128 + (60-i) : 128;
|
|
int sat = i < 60 ? 140 + i : i < 180 ? 200 : 200 - (i-180);
|
|
c.setHsl(239 - i, sat, light);
|
|
((quint8*)&m_histogramPalette[i])[0] = c.red();
|
|
((quint8*)&m_histogramPalette[i])[1] = c.green();
|
|
((quint8*)&m_histogramPalette[i])[2] = c.blue();
|
|
((quint8*)&m_histogramPalette[i])[3] = c.alpha();
|
|
}
|
|
|
|
// 4.2.3 palette
|
|
// for (int i = 1; i < 240; i++)
|
|
// {
|
|
// QColor c;
|
|
// int val = i < 60 ? 255 : 200;
|
|
// int sat = i < 60 ? 128 : i < 180 ? 255 : 180;
|
|
// c.setHsv(239 - i, sat, val);
|
|
// ((quint8*)&m_histogramPalette[i])[0] = c.red();
|
|
// ((quint8*)&m_histogramPalette[i])[1] = c.green();
|
|
// ((quint8*)&m_histogramPalette[i])[2] = c.blue();
|
|
// ((quint8*)&m_histogramPalette[i])[3] = c.alpha();
|
|
// }
|
|
|
|
// Original palette:
|
|
// for(int i = 16; i < 240; i++) {
|
|
// QColor c;
|
|
// c.setHsv(239 - i, 255 - ((i < 200) ? 0 : (i - 200) * 3), 150 + ((i < 100) ? i : 100));
|
|
// ((quint8*)&m_histogramPalette[i])[0] = c.red();
|
|
// ((quint8*)&m_histogramPalette[i])[1] = c.green();
|
|
// ((quint8*)&m_histogramPalette[i])[2] = c.blue();
|
|
// ((quint8*)&m_histogramPalette[i])[3] = c.alpha();
|
|
// }
|
|
// for(int i = 1; i < 16; i++) {
|
|
// QColor c;
|
|
// c.setHsv(255, 128, 48 + i * 4);
|
|
// ((quint8*)&m_histogramPalette[i])[0] = c.red();
|
|
// ((quint8*)&m_histogramPalette[i])[1] = c.green();
|
|
// ((quint8*)&m_histogramPalette[i])[2] = c.blue();
|
|
// ((quint8*)&m_histogramPalette[i])[3] = c.alpha();
|
|
// }
|
|
|
|
m_decayDivisor = 1;
|
|
m_decayDivisorCount = m_decayDivisor;
|
|
m_histogramStroke = 30;
|
|
|
|
m_timeScale.setFont(font());
|
|
m_timeScale.setOrientation(Qt::Vertical);
|
|
m_timeScale.setRange(Unit::Time, 0, 1);
|
|
m_powerScale.setFont(font());
|
|
m_powerScale.setOrientation(Qt::Vertical);
|
|
m_frequencyScale.setFont(font());
|
|
m_frequencyScale.setOrientation(Qt::Horizontal);
|
|
|
|
m_textOverlayFont = font(); // QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
|
m_textOverlayFont.setBold(true);
|
|
// m_textOverlayFont.setPointSize(font().pointSize() - 1);
|
|
resetFrequencyZoom();
|
|
|
|
m_timer.setTimerType(Qt::PreciseTimer);
|
|
connect(&m_timer, SIGNAL(timeout()), this, SLOT(tick()));
|
|
m_timer.start(m_fpsPeriodMs);
|
|
|
|
// Handle KeyEvents
|
|
setFocusPolicy(Qt::StrongFocus);
|
|
installEventFilter(this);
|
|
|
|
grabGesture(Qt::PinchGesture);
|
|
}
|
|
|
|
GLSpectrumView::~GLSpectrumView()
|
|
{
|
|
QMutexLocker mutexLocker(&m_mutex);
|
|
|
|
if (m_waterfallBuffer)
|
|
{
|
|
delete m_waterfallBuffer;
|
|
m_waterfallBuffer = nullptr;
|
|
}
|
|
|
|
if (m_3DSpectrogramBuffer)
|
|
{
|
|
delete m_3DSpectrogramBuffer;
|
|
m_3DSpectrogramBuffer = nullptr;
|
|
}
|
|
|
|
if (m_histogramBuffer)
|
|
{
|
|
delete m_histogramBuffer;
|
|
m_histogramBuffer = nullptr;
|
|
}
|
|
|
|
if (m_histogram)
|
|
{
|
|
delete[] m_histogram;
|
|
m_histogram = nullptr;
|
|
}
|
|
|
|
if (m_openGLLogger)
|
|
{
|
|
delete m_openGLLogger;
|
|
m_openGLLogger = nullptr;
|
|
}
|
|
|
|
clearSpectrumBuffer();
|
|
}
|
|
|
|
void GLSpectrumView::queueRequestCenterFrequency(qint64 frequency)
|
|
{
|
|
if (!m_frequencyRequested)
|
|
{
|
|
m_frequencyRequested = true;
|
|
m_requestedFrequency = frequency;
|
|
emit requestCenterFrequency(frequency);
|
|
}
|
|
else
|
|
{
|
|
m_nextFrequencyValid = true;
|
|
m_nextFrequency = frequency;
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::setCenterFrequency(qint64 frequency)
|
|
{
|
|
m_mutex.lock();
|
|
m_centerFrequency = frequency;
|
|
|
|
// Handle queued frequency requests
|
|
if (m_frequencyRequested && (frequency == m_requestedFrequency))
|
|
{
|
|
m_frequencyRequested = false;
|
|
if (m_nextFrequencyValid)
|
|
{
|
|
m_nextFrequencyValid = false;
|
|
queueRequestCenterFrequency(m_nextFrequency);
|
|
}
|
|
}
|
|
|
|
if (m_useCalibration) {
|
|
updateCalibrationPoints();
|
|
}
|
|
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setReferenceLevel(Real referenceLevel)
|
|
{
|
|
m_mutex.lock();
|
|
m_referenceLevel = referenceLevel;
|
|
m_changesPending = true;
|
|
redrawWaterfallAnd3DSpectrogram();
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setPowerRange(Real powerRange)
|
|
{
|
|
m_mutex.lock();
|
|
m_powerRange = powerRange;
|
|
m_changesPending = true;
|
|
redrawWaterfallAnd3DSpectrogram();
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setDecay(int decay)
|
|
{
|
|
m_decay = decay < 0 ? 0 : decay > 20 ? 20 : decay;
|
|
}
|
|
|
|
void GLSpectrumView::setDecayDivisor(int decayDivisor)
|
|
{
|
|
m_decayDivisor = decayDivisor < 1 ? 1 : decayDivisor > 20 ? 20 : decayDivisor;
|
|
}
|
|
|
|
void GLSpectrumView::setHistoStroke(int stroke)
|
|
{
|
|
m_histogramStroke = stroke < 1 ? 1 : stroke > 60 ? 60 : stroke;
|
|
}
|
|
|
|
void GLSpectrumView::setSampleRate(qint32 sampleRate)
|
|
{
|
|
m_mutex.lock();
|
|
m_sampleRate = sampleRate;
|
|
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportSampleRate(m_sampleRate));
|
|
}
|
|
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setTimingRate(qint32 timingRate)
|
|
{
|
|
m_mutex.lock();
|
|
m_timingRate = timingRate;
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setFFTOverlap(int overlap)
|
|
{
|
|
m_mutex.lock();
|
|
m_fftOverlap = overlap;
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setDisplayWaterfall(bool display)
|
|
{
|
|
m_mutex.lock();
|
|
m_displayWaterfall = display;
|
|
if (!display)
|
|
{
|
|
m_waterfallMarkers.clear();
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportWaterfallMarkersChange());
|
|
}
|
|
}
|
|
m_changesPending = true;
|
|
stopDrag();
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setDisplay3DSpectrogram(bool display)
|
|
{
|
|
m_mutex.lock();
|
|
m_display3DSpectrogram = display;
|
|
m_changesPending = true;
|
|
stopDrag();
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setSpectrumStyle(SpectrumSettings::SpectrumStyle style)
|
|
{
|
|
m_spectrumStyle = style;
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::set3DSpectrogramStyle(SpectrumSettings::SpectrogramStyle style)
|
|
{
|
|
m_3DSpectrogramStyle = style;
|
|
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();
|
|
}
|
|
|
|
void GLSpectrumView::setSsbSpectrum(bool ssbSpectrum)
|
|
{
|
|
m_ssbSpectrum = ssbSpectrum;
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setLsbDisplay(bool lsbDisplay)
|
|
{
|
|
m_lsbDisplay = lsbDisplay;
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setInvertedWaterfall(bool inv)
|
|
{
|
|
m_mutex.lock();
|
|
m_invertedWaterfall = inv;
|
|
m_changesPending = true;
|
|
stopDrag();
|
|
if (m_scrollBar) {
|
|
m_scrollBar->setInvertedAppearance(!inv);
|
|
}
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setDisplayMaxHold(bool display)
|
|
{
|
|
m_mutex.lock();
|
|
m_displayMaxHold = display;
|
|
if (!m_displayMaxHold && !m_displayCurrent && !m_displayHistogram)
|
|
{
|
|
m_histogramMarkers.clear();
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportHistogramMarkersChange());
|
|
}
|
|
}
|
|
m_changesPending = true;
|
|
stopDrag();
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setDisplayCurrent(bool display)
|
|
{
|
|
m_mutex.lock();
|
|
m_displayCurrent = display;
|
|
if (!m_displayMaxHold && !m_displayCurrent && !m_displayHistogram)
|
|
{
|
|
m_histogramMarkers.clear();
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportHistogramMarkersChange());
|
|
}
|
|
}
|
|
m_changesPending = true;
|
|
stopDrag();
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setDisplayHistogram(bool display)
|
|
{
|
|
m_mutex.lock();
|
|
m_displayHistogram = display;
|
|
if (!m_displayMaxHold && !m_displayCurrent && !m_displayHistogram)
|
|
{
|
|
m_histogramMarkers.clear();
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportHistogramMarkersChange());
|
|
}
|
|
}
|
|
m_changesPending = true;
|
|
stopDrag();
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setDisplayGrid(bool display)
|
|
{
|
|
m_displayGrid = display;
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setDisplayGridIntensity(int intensity)
|
|
{
|
|
m_displayGridIntensity = intensity;
|
|
|
|
if (m_displayGridIntensity > 100) {
|
|
m_displayGridIntensity = 100;
|
|
} else if (m_displayGridIntensity < 0) {
|
|
m_displayGridIntensity = 0;
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setDisplayTraceIntensity(int intensity)
|
|
{
|
|
m_displayTraceIntensity = intensity;
|
|
|
|
if (m_displayTraceIntensity > 100) {
|
|
m_displayTraceIntensity = 100;
|
|
} else if (m_displayTraceIntensity < 0) {
|
|
m_displayTraceIntensity = 0;
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setFreqScaleTruncationMode(bool mode)
|
|
{
|
|
m_frequencyScale.setTruncateMode(mode);
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setLinear(bool linear)
|
|
{
|
|
m_mutex.lock();
|
|
m_linear = linear;
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setUseCalibration(bool useCalibration)
|
|
{
|
|
m_mutex.lock();
|
|
m_useCalibration = useCalibration;
|
|
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportCalibrationShift(m_useCalibration ? m_calibrationShiftdB : 0.0));
|
|
}
|
|
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setMeasurementParams(
|
|
SpectrumSettings::Measurement measurement,
|
|
int centerFrequencyOffset,
|
|
int bandwidth,
|
|
int chSpacing,
|
|
int adjChBandwidth,
|
|
int harmonics,
|
|
int peaks,
|
|
bool highlight,
|
|
int precision,
|
|
unsigned memoryMask
|
|
)
|
|
{
|
|
m_mutex.lock();
|
|
m_measurement = measurement;
|
|
m_measurementCenterFrequencyOffset = centerFrequencyOffset;
|
|
m_measurementBandwidth = bandwidth;
|
|
m_measurementChSpacing = chSpacing;
|
|
m_measurementAdjChBandwidth = adjChBandwidth;
|
|
m_measurementHarmonics = harmonics;
|
|
m_measurementPeaks = peaks;
|
|
m_measurementHighlight = highlight;
|
|
m_measurementPrecision = precision;
|
|
m_measurementMemMasks = memoryMask;
|
|
m_changesPending = true;
|
|
if (m_measurements) {
|
|
m_measurements->setMeasurementParams(measurement, peaks, precision, memoryMask);
|
|
}
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::resetMeasurements()
|
|
{
|
|
m_mutex.lock();
|
|
for (int 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();
|
|
connect(channelMarker, SIGNAL(changedByAPI()), this, SLOT(channelMarkerChanged()));
|
|
connect(channelMarker, SIGNAL(destroyed(QObject*)), this, SLOT(channelMarkerDestroyed(QObject*)));
|
|
m_channelMarkerStates.append(new ChannelMarkerState(channelMarker));
|
|
m_changesPending = true;
|
|
stopDrag();
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::removeChannelMarker(ChannelMarker* channelMarker)
|
|
{
|
|
m_mutex.lock();
|
|
|
|
for (int i = 0; i < m_channelMarkerStates.size(); ++i)
|
|
{
|
|
if (m_channelMarkerStates[i]->m_channelMarker == channelMarker)
|
|
{
|
|
channelMarker->disconnect(this);
|
|
delete m_channelMarkerStates.takeAt(i);
|
|
m_changesPending = true;
|
|
stopDrag();
|
|
m_mutex.unlock();
|
|
update();
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_mutex.unlock();
|
|
}
|
|
|
|
void GLSpectrumView::setHistogramMarkers(const QList<SpectrumHistogramMarker>& histogramMarkers)
|
|
{
|
|
m_mutex.lock();
|
|
m_histogramMarkers = histogramMarkers;
|
|
updateHistogramMarkers();
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setWaterfallMarkers(const QList<SpectrumWaterfallMarker>& waterfallMarkers)
|
|
{
|
|
m_mutex.lock();
|
|
m_waterfallMarkers = waterfallMarkers;
|
|
updateWaterfallMarkers();
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setAnnotationMarkers(const QList<SpectrumAnnotationMarker>& annotationMarkers)
|
|
{
|
|
m_mutex.lock();
|
|
m_annotationMarkers = annotationMarkers;
|
|
updateAnnotationMarkers();
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setMarkersDisplay(SpectrumSettings::MarkersDisplay markersDisplay)
|
|
{
|
|
m_mutex.lock();
|
|
m_markersDisplay = markersDisplay;
|
|
updateMarkersDisplay();
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setCalibrationPoints(const QList<SpectrumCalibrationPoint>& calibrationPoints)
|
|
{
|
|
m_mutex.lock();
|
|
m_calibrationPoints = calibrationPoints;
|
|
updateCalibrationPoints();
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::setCalibrationInterpMode(SpectrumSettings::CalibrationInterpolationMode mode)
|
|
{
|
|
m_mutex.lock();
|
|
m_calibrationInterpMode = mode;
|
|
updateCalibrationPoints();
|
|
m_changesPending = true;
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
float GLSpectrumView::getPowerMax() const
|
|
{
|
|
return m_linear ? m_powerScale.getRangeMax() : CalcDb::powerFromdB(m_powerScale.getRangeMax());
|
|
}
|
|
|
|
float GLSpectrumView::getTimeMax() const
|
|
{
|
|
return m_timeScale.getRangeMax();
|
|
}
|
|
|
|
void GLSpectrumView::setWaterfallTimeFormat(SpectrumSettings::WaterfallTimeUnits waterfallTimeUnits, const QString& format)
|
|
{
|
|
QMutexLocker mutexLocker(&m_mutex);
|
|
|
|
m_waterfallTimeUnits = waterfallTimeUnits;
|
|
m_waterfallTimeFormat = format;
|
|
if (m_waterfallTimeUnits == SpectrumSettings::TimeOffset) {
|
|
m_timeScale.setTickFormatter(nullptr);
|
|
} else {
|
|
m_timeScale.setTickFormatter(this);
|
|
}
|
|
m_changesPending = true;
|
|
}
|
|
|
|
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() > 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<qint64>(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<QString, int> 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<qint64> frequencies;
|
|
QVector<float> 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<float> 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 (int 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 < 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 < 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 < m_spectrumBuffer.size())
|
|
{
|
|
updateHistogram(m_spectrumBuffer[idx].m_spectrum, m_fftSize, 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 < 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_fftSize, 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_fftSize, m_fftMin, m_nbBins);
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::updateSpectrumNoBuffer(const Real *spectrum, int fftSize)
|
|
{
|
|
if (m_spectrumNoBuffer.size() != 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() >= 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 <= 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[fftMin + i] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0);
|
|
v = clampWaterfall(v);
|
|
*pix++ = m_waterfallPalette[(int)v];
|
|
}
|
|
|
|
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::clear3DSpectrogramRow(int nbBins)
|
|
{
|
|
if (m_3DSpectrogramBufferPos < m_3DSpectrogramBuffer->height())
|
|
{
|
|
quint8* pix = (quint8*)m_3DSpectrogramBuffer->scanLine(m_3DSpectrogramBufferPos);
|
|
|
|
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[fftMin + i] - m_referenceLevel) * 2.4 * 100.0 / m_powerRange + 240.0);
|
|
v = clampPixel(v);
|
|
*pix++ = v;
|
|
}
|
|
|
|
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, int fftSize, int fftMin, int nbBins)
|
|
{
|
|
quint8* b = m_histogram;
|
|
int fftMulSize = 100 * nbBins;
|
|
|
|
if ((m_displayHistogram || m_displayMaxHold) && (m_decay != 0))
|
|
{
|
|
m_decayDivisorCount--;
|
|
|
|
if ((m_decay > 1) || (m_decayDivisorCount <= 0))
|
|
{
|
|
for (int i = 0; i < fftMulSize; i++)
|
|
{
|
|
if (*b > m_decay) {
|
|
*b = *b - m_decay;
|
|
} else {
|
|
*b = 0;
|
|
}
|
|
|
|
b++;
|
|
}
|
|
|
|
m_decayDivisorCount = m_decayDivisor;
|
|
}
|
|
}
|
|
|
|
#if 0 //def USE_SSE2
|
|
if(m_decay >= 0) { // normal
|
|
const __m128 refl = {m_referenceLevel, m_referenceLevel, m_referenceLevel, m_referenceLevel};
|
|
const __m128 power = {m_powerRange, m_powerRange, m_powerRange, m_powerRange};
|
|
const __m128 mul = {100.0f, 100.0f, 100.0f, 100.0f};
|
|
|
|
for(int i = 0; i < m_fftSize; i += 4) {
|
|
__m128 abc = _mm_loadu_ps (&spectrum[i]);
|
|
abc = _mm_sub_ps(abc, refl);
|
|
abc = _mm_mul_ps(abc, mul);
|
|
abc = _mm_div_ps(abc, power);
|
|
abc = _mm_add_ps(abc, mul);
|
|
__m128i result = _mm_cvtps_epi32(abc);
|
|
|
|
for(int j = 0; j < 4; j++) {
|
|
int v = ((int*)&result)[j];
|
|
if((v >= 0) && (v <= 99)) {
|
|
b = m_histogram + (i + j) * 100 + v;
|
|
if(*b < 220)
|
|
*b += m_histogramStroke; // was 4
|
|
else if(*b < 239)
|
|
*b += 1;
|
|
}
|
|
}
|
|
}
|
|
} else { // draw double pixels
|
|
int add = -m_decay * 4;
|
|
const __m128 refl = {m_referenceLevel, m_referenceLevel, m_referenceLevel, m_referenceLevel};
|
|
const __m128 power = {m_powerRange, m_powerRange, m_powerRange, m_powerRange};
|
|
const __m128 mul = {100.0f, 100.0f, 100.0f, 100.0f};
|
|
|
|
for(int i = 0; i < m_fftSize; i += 4) {
|
|
__m128 abc = _mm_loadu_ps (&spectrum[i]);
|
|
abc = _mm_sub_ps(abc, refl);
|
|
abc = _mm_mul_ps(abc, mul);
|
|
abc = _mm_div_ps(abc, power);
|
|
abc = _mm_add_ps(abc, mul);
|
|
__m128i result = _mm_cvtps_epi32(abc);
|
|
|
|
for(int j = 0; j < 4; j++) {
|
|
int v = ((int*)&result)[j];
|
|
if((v >= 1) && (v <= 98)) {
|
|
b = m_histogram + (i + j) * 100 + v;
|
|
if(b[-1] < 220)
|
|
b[-1] += add;
|
|
else if(b[-1] < 239)
|
|
b[-1] += 1;
|
|
if(b[0] < 220)
|
|
b[0] += add;
|
|
else if(b[0] < 239)
|
|
b[0] += 1;
|
|
if(b[1] < 220)
|
|
b[1] += add;
|
|
else if(b[1] < 239)
|
|
b[1] += 1;
|
|
} else if((v >= 0) && (v <= 99)) {
|
|
b = m_histogram + (i + j) * 100 + v;
|
|
if(*b < 220)
|
|
*b += add;
|
|
else if(*b < 239)
|
|
*b += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
for (int i = 0; i < nbBins; i++)
|
|
{
|
|
int v = (int)((spectrum[fftMin + i] - m_referenceLevel) * 100.0 / m_powerRange + 100.0);
|
|
|
|
if ((v >= 0) && (v <= 99))
|
|
{
|
|
b = m_histogram + i * 100 + v;
|
|
|
|
// capping to 239 as palette values are [0..239]
|
|
if (*b + m_histogramStroke <= 239) {
|
|
*b += m_histogramStroke; // was 4
|
|
} else {
|
|
*b = 239;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void GLSpectrumView::initializeGL()
|
|
{
|
|
QOpenGLContext *glCurrentContext = QOpenGLContext::currentContext();
|
|
int majorVersion = 0;
|
|
int minorVersion = 0;
|
|
|
|
if (glCurrentContext)
|
|
{
|
|
if (QOpenGLContext::currentContext()->isValid())
|
|
{
|
|
qDebug() << "GLSpectrumView::initializeGL: context:"
|
|
<< " major: " << (QOpenGLContext::currentContext()->format()).majorVersion()
|
|
<< " minor: " << (QOpenGLContext::currentContext()->format()).minorVersion()
|
|
<< " ES: " << (QOpenGLContext::currentContext()->isOpenGLES() ? "yes" : "no");
|
|
majorVersion = (QOpenGLContext::currentContext()->format()).majorVersion();
|
|
minorVersion = (QOpenGLContext::currentContext()->format()).minorVersion();
|
|
}
|
|
else {
|
|
qDebug() << "GLSpectrumView::initializeGL: current context is invalid";
|
|
}
|
|
|
|
// Enable OpenGL debugging
|
|
// Disable for release, as some OpenGL drivers are quite verbose and output
|
|
// info on every frame
|
|
if (false)
|
|
{
|
|
QSurfaceFormat format = glCurrentContext->format();
|
|
format.setOption(QSurfaceFormat::DebugContext);
|
|
glCurrentContext->setFormat(format);
|
|
|
|
if (glCurrentContext->hasExtension(QByteArrayLiteral("GL_KHR_debug")))
|
|
{
|
|
m_openGLLogger = new QOpenGLDebugLogger(this);
|
|
m_openGLLogger->initialize();
|
|
connect(m_openGLLogger, &QOpenGLDebugLogger::messageLogged, this, &GLSpectrumView::openGLDebug);
|
|
m_openGLLogger->startLogging(QOpenGLDebugLogger::SynchronousLogging);
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "GLSpectrumView::initializeGL: GL_KHR_debug not available";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qCritical() << "GLSpectrumView::initializeGL: no current context";
|
|
return;
|
|
}
|
|
|
|
QOpenGLFunctions *glFunctions = QOpenGLContext::currentContext()->functions();
|
|
glFunctions->initializeOpenGLFunctions();
|
|
|
|
//glDisable(GL_DEPTH_TEST);
|
|
m_glShaderSimple.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderLeftScale.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderFrequencyScale.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderWaterfall.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderHistogram.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderColorMap.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderTextOverlay.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderInfo.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderSpectrogram.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderSpectrogramTimeScale.initializeGL(majorVersion, minorVersion);
|
|
m_glShaderSpectrogramPowerScale.initializeGL(majorVersion, minorVersion);
|
|
}
|
|
|
|
void GLSpectrumView::openGLDebug(const QOpenGLDebugMessage &debugMessage)
|
|
{
|
|
qDebug() << "GLSpectrumView::openGLDebug: " << debugMessage;
|
|
}
|
|
|
|
void GLSpectrumView::resizeGL(int width, int height)
|
|
{
|
|
QMutexLocker mutexLocker(&m_mutex);
|
|
QOpenGLFunctions *glFunctions = QOpenGLContext::currentContext()->functions();
|
|
glFunctions->glViewport(0, 0, width, height);
|
|
m_changesPending = true;
|
|
}
|
|
|
|
void GLSpectrumView::clearSpectrumHistogram()
|
|
{
|
|
if (!m_mutex.tryLock(2)) {
|
|
return;
|
|
}
|
|
|
|
memset(m_histogram, 0x00, 100 * m_nbBins);
|
|
|
|
m_mutex.unlock();
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::paintGL()
|
|
{
|
|
PROFILER_START()
|
|
|
|
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();
|
|
m_changesPending = false;
|
|
}
|
|
|
|
if (m_nbBins <= 0)
|
|
{
|
|
m_mutex.unlock();
|
|
return;
|
|
}
|
|
|
|
QOpenGLFunctions *glFunctions = QOpenGLContext::currentContext()->functions();
|
|
glFunctions->glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
glFunctions->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
|
|
QMatrix4x4 spectrogramGridMatrix;
|
|
float devicePixelRatio;
|
|
|
|
if (m_display3DSpectrogram)
|
|
{
|
|
m_glShaderSpectrogram.applyTransform(spectrogramGridMatrix);
|
|
// paint 3D spectrogram
|
|
if (m_3DSpectrogramTexturePos + m_3DSpectrogramBufferPos < m_3DSpectrogramTextureHeight)
|
|
{
|
|
m_glShaderSpectrogram.subTexture(0, m_3DSpectrogramTexturePos, m_nbBins + 1, m_3DSpectrogramBufferPos, m_3DSpectrogramBuffer->scanLine(0));
|
|
m_3DSpectrogramTexturePos += m_3DSpectrogramBufferPos;
|
|
}
|
|
else
|
|
{
|
|
int breakLine = m_3DSpectrogramTextureHeight - m_3DSpectrogramTexturePos;
|
|
int linesLeft = m_3DSpectrogramTexturePos + m_3DSpectrogramBufferPos - m_3DSpectrogramTextureHeight;
|
|
m_glShaderSpectrogram.subTexture(0, m_3DSpectrogramTexturePos, m_nbBins + 1, breakLine, m_3DSpectrogramBuffer->scanLine(0));
|
|
m_glShaderSpectrogram.subTexture(0, 0, m_nbBins + 1, linesLeft, m_3DSpectrogramBuffer->scanLine(breakLine));
|
|
m_3DSpectrogramTexturePos = linesLeft;
|
|
}
|
|
|
|
m_3DSpectrogramBufferPos = 0;
|
|
|
|
float prop_y = m_3DSpectrogramTexturePos / (m_3DSpectrogramTextureHeight - 1.0);
|
|
|
|
// Temporarily reduce viewport to waterfall area so anything outside is clipped
|
|
if (window()->windowHandle()) {
|
|
devicePixelRatio = window()->windowHandle()->devicePixelRatio();
|
|
} else {
|
|
devicePixelRatio = 1.0f;
|
|
}
|
|
glFunctions->glViewport(0, m_3DSpectrogramBottom*devicePixelRatio, width()*devicePixelRatio, m_waterfallHeight*devicePixelRatio);
|
|
m_glShaderSpectrogram.drawSurface(m_3DSpectrogramStyle, spectrogramGridMatrix, prop_y, m_invertedWaterfall);
|
|
glFunctions->glViewport(0, 0, width()*devicePixelRatio, height()*devicePixelRatio);
|
|
}
|
|
else if (m_displayWaterfall)
|
|
{
|
|
// paint 2D waterfall
|
|
{
|
|
GLfloat vtx1[] = {
|
|
0, m_invertedWaterfall ? 0.0f : 1.0f,
|
|
1, m_invertedWaterfall ? 0.0f : 1.0f,
|
|
1, m_invertedWaterfall ? 1.0f : 0.0f,
|
|
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));
|
|
m_waterfallTexturePos += m_waterfallBufferPos;
|
|
}
|
|
else
|
|
{
|
|
int breakLine = m_waterfallTextureHeight - m_waterfallTexturePos;
|
|
int linesLeft = m_waterfallTexturePos + m_waterfallBufferPos - m_waterfallTextureHeight;
|
|
m_glShaderWaterfall.subTexture(0, m_waterfallTexturePos, m_nbBins + 1, breakLine, m_waterfallBuffer->scanLine(0));
|
|
m_glShaderWaterfall.subTexture(0, 0, m_nbBins + 1, linesLeft, m_waterfallBuffer->scanLine(breakLine));
|
|
m_waterfallTexturePos = linesLeft;
|
|
}
|
|
|
|
m_waterfallBufferPos = 0;
|
|
|
|
float prop_y = m_waterfallTexturePos / (m_waterfallTextureHeight - 1.0);
|
|
float off = 1.0 / (m_waterfallTextureHeight - 1.0);
|
|
|
|
GLfloat tex1[] = {
|
|
0, prop_y + 1 - off,
|
|
1, prop_y + 1 - off,
|
|
1, prop_y,
|
|
0, prop_y
|
|
};
|
|
|
|
m_glShaderWaterfall.drawSurface(m_glWaterfallBoxMatrix, tex1, vtx1, 4);
|
|
}
|
|
|
|
// paint channels
|
|
if (m_mouseInside)
|
|
{
|
|
for (int i = 0; i < m_channelMarkerStates.size(); ++i)
|
|
{
|
|
ChannelMarkerState* dv = m_channelMarkerStates[i];
|
|
|
|
if (dv->m_channelMarker->getVisible()
|
|
&& (dv->m_channelMarker->getSourceOrSinkStream() == m_displaySourceOrSink)
|
|
&& dv->m_channelMarker->streamIndexApplies(m_displayStreamIndex))
|
|
{
|
|
{
|
|
GLfloat q3[] {
|
|
0, 0,
|
|
1, 0,
|
|
1, 1,
|
|
0, 1,
|
|
0.5, 0,
|
|
0.5, 1,
|
|
};
|
|
|
|
QVector4D color(dv->m_channelMarker->getColor().redF(), dv->m_channelMarker->getColor().greenF(), dv->m_channelMarker->getColor().blueF(), 0.3f);
|
|
m_glShaderSimple.drawSurface(dv->m_glMatrixWaterfall, color, q3, 4);
|
|
|
|
QVector4D colorLine(0.8f, 0.8f, 0.6f, 1.0f);
|
|
m_glShaderSimple.drawSegments(dv->m_glMatrixDsbWaterfall, colorLine, &q3[8], 2);
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// draw rect around
|
|
{
|
|
GLfloat q3[] {
|
|
1, 1,
|
|
0, 1,
|
|
0, 0,
|
|
1, 0
|
|
};
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, 0.5f);
|
|
m_glShaderSimple.drawContour(m_glWaterfallBoxMatrix, color, q3, 4);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
if (m_displayHistogram)
|
|
{
|
|
{
|
|
// import new lines into the texture
|
|
quint32* pix;
|
|
quint8* bs = m_histogram;
|
|
|
|
for (int y = 0; y < 100; y++)
|
|
{
|
|
quint8* b = bs;
|
|
pix = (quint32*)m_histogramBuffer->scanLine(99 - y);
|
|
|
|
for (int x = 0; x < m_nbBins; x++)
|
|
{
|
|
*pix = m_histogramPalette[*b];
|
|
pix++;
|
|
b += 100;
|
|
}
|
|
|
|
bs++;
|
|
}
|
|
|
|
GLfloat vtx1[] = {
|
|
0, 0,
|
|
1, 0,
|
|
1, 1,
|
|
0, 1
|
|
};
|
|
GLfloat tex1[] = {
|
|
0, 0,
|
|
1, 0,
|
|
1, 1,
|
|
0, 1
|
|
};
|
|
|
|
m_glShaderHistogram.subTexture(0, 0, m_nbBins, 100, m_histogramBuffer->scanLine(0));
|
|
m_glShaderHistogram.drawSurface(m_glHistogramBoxMatrix, tex1, vtx1, 4);
|
|
}
|
|
}
|
|
|
|
|
|
// paint channels
|
|
if (m_mouseInside)
|
|
{
|
|
// Effective BW overlays
|
|
for (int i = 0; i < m_channelMarkerStates.size(); ++i)
|
|
{
|
|
ChannelMarkerState* dv = m_channelMarkerStates[i];
|
|
|
|
if (dv->m_channelMarker->getVisible()
|
|
&& (dv->m_channelMarker->getSourceOrSinkStream() == m_displaySourceOrSink)
|
|
&& dv->m_channelMarker->streamIndexApplies(m_displayStreamIndex))
|
|
{
|
|
{
|
|
GLfloat q3[] {
|
|
0, 0,
|
|
1, 0,
|
|
1, 1,
|
|
0, 1,
|
|
0.5, 0,
|
|
0.5, 1
|
|
};
|
|
|
|
QVector4D color(dv->m_channelMarker->getColor().redF(), dv->m_channelMarker->getColor().greenF(), dv->m_channelMarker->getColor().blueF(), 0.3f);
|
|
m_glShaderSimple.drawSurface(dv->m_glMatrixHistogram, color, q3, 4);
|
|
|
|
QVector4D colorLine(0.8f, 0.8f, 0.6f, 1.0f);
|
|
|
|
if (dv->m_channelMarker->getSidebands() != ChannelMarker::dsb) {
|
|
q3[6] = 0.5;
|
|
}
|
|
|
|
m_glShaderSimple.drawSegments(dv->m_glMatrixDsbHistogram, colorLine, &q3[8], 2);
|
|
m_glShaderSimple.drawSegments(dv->m_glMatrixFreqScale, colorLine, q3, 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// paint left scales (time and power)
|
|
if (m_displayWaterfall || m_displayMaxHold || m_displayCurrent || m_displayHistogram )
|
|
{
|
|
{
|
|
GLfloat vtx1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
GLfloat tex1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
|
|
m_glShaderLeftScale.drawSurface(m_glLeftScaleBoxMatrix, tex1, vtx1, 4);
|
|
}
|
|
}
|
|
|
|
// paint frequency scale
|
|
if (m_displayWaterfall || m_displayMaxHold || m_displayCurrent || m_displayHistogram)
|
|
{
|
|
{
|
|
GLfloat vtx1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
GLfloat tex1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
|
|
m_glShaderFrequencyScale.drawSurface(m_glFrequencyScaleBoxMatrix, tex1, vtx1, 4);
|
|
}
|
|
|
|
// paint channels
|
|
|
|
// Effective bandwidth overlays
|
|
for (int i = 0; i < m_channelMarkerStates.size(); ++i)
|
|
{
|
|
ChannelMarkerState* dv = m_channelMarkerStates[i];
|
|
|
|
// frequency scale channel overlay
|
|
if (dv->m_channelMarker->getVisible()
|
|
&& (dv->m_channelMarker->getSourceOrSinkStream() == m_displaySourceOrSink)
|
|
&& dv->m_channelMarker->streamIndexApplies(m_displayStreamIndex))
|
|
{
|
|
{
|
|
GLfloat q3[] {
|
|
1, 0.2,
|
|
0, 0.2,
|
|
0, 0,
|
|
1, 0,
|
|
0.5, 0,
|
|
0.5, 1
|
|
};
|
|
|
|
QVector4D color(dv->m_channelMarker->getColor().redF(), dv->m_channelMarker->getColor().greenF(), dv->m_channelMarker->getColor().blueF(), 0.5f);
|
|
m_glShaderSimple.drawSurface(dv->m_glMatrixFreqScale, color, q3, 4);
|
|
|
|
if (dv->m_channelMarker->getHighlighted())
|
|
{
|
|
QVector4D colorLine(0.8f, 0.8f, 0.6f, 1.0f);
|
|
m_glShaderSimple.drawSegments(dv->m_glMatrixDsbFreqScale, colorLine, &q3[8], 2);
|
|
m_glShaderSimple.drawSegments(dv->m_glMatrixFreqScale, colorLine, &q3[4], 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// paint 3D spectrogram scales
|
|
if (m_display3DSpectrogram && m_displayGrid)
|
|
{
|
|
glFunctions->glViewport(0, m_3DSpectrogramBottom*devicePixelRatio, width()*devicePixelRatio, m_waterfallHeight*devicePixelRatio);
|
|
{
|
|
GLfloat l = m_spectrogramTimePixmap.width() / (GLfloat) width();
|
|
GLfloat r = m_rightMargin / (GLfloat) width();
|
|
GLfloat h = m_frequencyPixmap.height() / (GLfloat) m_waterfallHeight;
|
|
|
|
GLfloat vtx1[] = {
|
|
-l, - h,
|
|
1.0f+r, -h,
|
|
1.0f+r, 0.0f,
|
|
-l, 0.0f
|
|
};
|
|
GLfloat tex1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
|
|
m_glShaderFrequencyScale.drawSurface(spectrogramGridMatrix, tex1, vtx1, 4);
|
|
}
|
|
|
|
{
|
|
GLfloat w = m_spectrogramTimePixmap.width() / (GLfloat) width();
|
|
GLfloat h = (m_bottomMargin/2) / (GLfloat) m_waterfallHeight; // m_bottomMargin is fm.ascent
|
|
|
|
GLfloat vtx1[] = {
|
|
-w, 0.0f-h,
|
|
0.0f, 0.0f-h,
|
|
0.0f, 1.0f+h,
|
|
-w, 1.0f+h
|
|
};
|
|
GLfloat tex1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
|
|
m_glShaderSpectrogramTimeScale.drawSurface(spectrogramGridMatrix, tex1, vtx1, 4);
|
|
}
|
|
|
|
{
|
|
GLfloat w = m_spectrogramPowerPixmap.width() / (GLfloat) width();
|
|
GLfloat h = m_topMargin / (GLfloat) m_spectrogramPowerPixmap.height();
|
|
|
|
GLfloat vtx1[] = {
|
|
-w, 1.0f, 0.0f,
|
|
0.0f, 1.0f, 0.0f,
|
|
0.0f, 1.0f, 1.0f+h,
|
|
-w, 1.0f, 1.0f+h,
|
|
};
|
|
GLfloat tex1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
|
|
m_glShaderSpectrogramPowerScale.drawSurface(spectrogramGridMatrix, tex1, vtx1, 4, 3);
|
|
}
|
|
|
|
glFunctions->glViewport(0, 0, width()*devicePixelRatio, height()*devicePixelRatio);
|
|
}
|
|
|
|
// paint max hold lines on top of histogram
|
|
if (m_displayMaxHold)
|
|
{
|
|
if (m_maxHold.size() < (uint) m_nbBins) {
|
|
m_maxHold.resize(m_nbBins);
|
|
}
|
|
|
|
for (int i = 0; i < m_nbBins; i++)
|
|
{
|
|
int j;
|
|
quint8* bs = m_histogram + i * 100;
|
|
|
|
for (j = 99; j >= 0; j--)
|
|
{
|
|
if (bs[j] > 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// m_referenceLevel : top
|
|
// m_referenceLevel - m_powerRange : bottom
|
|
m_maxHold[i] = ((j - 99) * m_powerRange) / 99.0 + m_referenceLevel;
|
|
}
|
|
// Fill under max hold line
|
|
if (m_spectrumStyle != SpectrumSettings::Line)
|
|
{
|
|
GLfloat *q3 = m_q3ColorMap.m_array;
|
|
for (int i = 0; i < m_nbBins; i++)
|
|
{
|
|
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;
|
|
}
|
|
q3[4*m_nbBins] = (GLfloat) m_nbBins;
|
|
q3[4*m_nbBins+1] = -m_powerRange;
|
|
q3[4*m_nbBins+2] = (GLfloat) m_nbBins;
|
|
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));
|
|
}
|
|
// Max hold line
|
|
{
|
|
GLfloat *q3 = m_q3FFT.m_array;
|
|
|
|
for (int i = 0; i < m_nbBins; i++)
|
|
{
|
|
Real v = clampPower(m_maxHold[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(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() == 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)
|
|
{
|
|
Real bottom = -m_powerRange;
|
|
GLfloat *q3;
|
|
|
|
if (m_spectrumStyle != SpectrumSettings::Line)
|
|
{
|
|
q3 = m_q3ColorMap.m_array;
|
|
// Fill under line
|
|
for (int i = 0; i < m_nbBins; i++)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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 {
|
|
m_glShaderSimple.drawSurfaceStrip(m_glHistogramSpectrumMatrix, color, q3, 2*(m_nbBins+1));
|
|
}
|
|
}
|
|
|
|
{
|
|
if (m_histogramFindPeaks) {
|
|
m_peakFinder.init(m_currentSpectrum[m_fftMin]);
|
|
}
|
|
|
|
// Draw line
|
|
q3 = m_q3FFT.m_array;
|
|
for (int i = 0; i < m_nbBins; i++)
|
|
{
|
|
Real q1 = m_currentSpectrum[0];
|
|
Real q2 = m_currentSpectrum[m_fftMin + i];
|
|
Real q = m_currentSpectrum[m_fftMin + i] - m_referenceLevel;
|
|
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[m_fftMin + i], i == m_nbBins - 1);
|
|
}
|
|
}
|
|
|
|
q3[2*m_nbBins] = (GLfloat) m_nbBins;
|
|
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(m_spectrumColor.redF(), m_spectrumColor.greenF(), m_spectrumColor.blueF(), (float) m_displayTraceIntensity / 100.0f);
|
|
}
|
|
m_glShaderSimple.drawPolyline(m_glHistogramSpectrumMatrix, color, q3, m_nbBins+1);
|
|
|
|
if (m_histogramFindPeaks) {
|
|
m_peakFinder.sortPeaks();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_displayCurrent && m_currentSpectrum && (m_markersDisplay & SpectrumSettings::MarkersDisplaySpectrum))
|
|
{
|
|
if (m_histogramFindPeaks) {
|
|
updateHistogramPeaks();
|
|
}
|
|
|
|
drawSpectrumMarkers();
|
|
}
|
|
|
|
if (m_markersDisplay & SpectrumSettings::MarkersDisplayAnnotations) {
|
|
drawAnnotationMarkers();
|
|
}
|
|
|
|
// paint waterfall grid
|
|
if (m_displayWaterfall && m_displayGrid)
|
|
{
|
|
const ScaleEngine::TickList* tickList;
|
|
const ScaleEngine::Tick* tick;
|
|
tickList = &m_timeScale.getTickList();
|
|
|
|
{
|
|
GLfloat *q3 = m_q3TickTime.m_array;
|
|
int effectiveTicks = 0;
|
|
|
|
for (int i= 0; i < tickList->count(); i++)
|
|
{
|
|
tick = &(*tickList)[i];
|
|
|
|
if (tick->major)
|
|
{
|
|
if (tick->textSize > 0)
|
|
{
|
|
float y = tick->pos / m_timeScale.getSize();
|
|
q3[4*effectiveTicks] = 0;
|
|
q3[4*effectiveTicks+1] = y;
|
|
q3[4*effectiveTicks+2] = 1;
|
|
q3[4*effectiveTicks+3] = y;
|
|
effectiveTicks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, (float) m_displayGridIntensity / 100.0f);
|
|
m_glShaderSimple.drawSegments(m_glWaterfallBoxMatrix, color, q3, 2*effectiveTicks);
|
|
}
|
|
|
|
tickList = &m_frequencyScale.getTickList();
|
|
|
|
{
|
|
GLfloat *q3 = m_q3TickFrequency.m_array;
|
|
int effectiveTicks = 0;
|
|
|
|
for (int i= 0; i < tickList->count(); i++)
|
|
{
|
|
tick = &(*tickList)[i];
|
|
|
|
if (tick->major)
|
|
{
|
|
if (tick->textSize > 0)
|
|
{
|
|
float x = tick->pos / m_frequencyScale.getSize();
|
|
q3[4*effectiveTicks] = x;
|
|
q3[4*effectiveTicks+1] = 0;
|
|
q3[4*effectiveTicks+2] = x;
|
|
q3[4*effectiveTicks+3] = 1;
|
|
effectiveTicks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, (float) m_displayGridIntensity / 100.0f);
|
|
m_glShaderSimple.drawSegments(m_glWaterfallBoxMatrix, color, q3, 2*effectiveTicks);
|
|
}
|
|
}
|
|
|
|
// paint 3D spectrogram grid - this is drawn on top of signal, so that appears slightly transparent
|
|
// x-axis is freq, y time and z power
|
|
if (m_displayGrid && m_display3DSpectrogram)
|
|
{
|
|
const ScaleEngine::TickList* tickList;
|
|
const ScaleEngine::Tick* tick;
|
|
|
|
glFunctions->glViewport(0, m_3DSpectrogramBottom*devicePixelRatio, width()*devicePixelRatio, m_waterfallHeight*devicePixelRatio);
|
|
|
|
tickList = &m_powerScale.getTickList();
|
|
{
|
|
GLfloat *q3 = m_q3TickPower.m_array;
|
|
int effectiveTicks = 0;
|
|
|
|
for (int i= 0; i < tickList->count(); i++)
|
|
{
|
|
tick = &(*tickList)[i];
|
|
|
|
if (tick->major)
|
|
{
|
|
if (tick->textSize > 0)
|
|
{
|
|
float y = tick->pos / m_powerScale.getSize();
|
|
q3[6*effectiveTicks] = 0.0;
|
|
q3[6*effectiveTicks+1] = 1.0;
|
|
q3[6*effectiveTicks+2] = y;
|
|
q3[6*effectiveTicks+3] = 1.0;
|
|
q3[6*effectiveTicks+4] = 1.0;
|
|
q3[6*effectiveTicks+5] = y;
|
|
effectiveTicks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, (float) m_displayGridIntensity / 100.0f);
|
|
m_glShaderSimple.drawSegments(spectrogramGridMatrix, color, q3, 2*effectiveTicks, 3);
|
|
}
|
|
|
|
tickList = &m_timeScale.getTickList();
|
|
{
|
|
GLfloat *q3 = m_q3TickTime.m_array;
|
|
int effectiveTicks = 0;
|
|
|
|
for (int i= 0; i < tickList->count(); i++)
|
|
{
|
|
tick = &(*tickList)[i];
|
|
|
|
if (tick->major)
|
|
{
|
|
if (tick->textSize > 0)
|
|
{
|
|
float y = tick->pos / m_timeScale.getSize();
|
|
q3[4*effectiveTicks] = 0.0;
|
|
q3[4*effectiveTicks+1] = 1.0 - y;
|
|
q3[4*effectiveTicks+2] = 1.0;
|
|
q3[4*effectiveTicks+3] = 1.0 - y;
|
|
effectiveTicks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, (float) m_displayGridIntensity / 100.0f);
|
|
m_glShaderSimple.drawSegments(spectrogramGridMatrix, color, q3, 2*effectiveTicks);
|
|
}
|
|
|
|
tickList = &m_frequencyScale.getTickList();
|
|
{
|
|
GLfloat *q3 = m_q3TickFrequency.m_array;
|
|
int effectiveTicks = 0;
|
|
|
|
for (int i= 0; i < tickList->count(); i++)
|
|
{
|
|
tick = &(*tickList)[i];
|
|
|
|
if (tick->major)
|
|
{
|
|
if (tick->textSize > 0)
|
|
{
|
|
float x = tick->pos / m_frequencyScale.getSize();
|
|
q3[4*effectiveTicks] = x;
|
|
q3[4*effectiveTicks+1] = -0.0;
|
|
q3[4*effectiveTicks+2] = x;
|
|
q3[4*effectiveTicks+3] = 1.0;
|
|
effectiveTicks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, (float) m_displayGridIntensity / 100.0f);
|
|
m_glShaderSimple.drawSegments(spectrogramGridMatrix, color, q3, 2*effectiveTicks);
|
|
}
|
|
{
|
|
GLfloat *q3 = m_q3TickFrequency.m_array;
|
|
int effectiveTicks = 0;
|
|
|
|
for (int i= 0; i < tickList->count(); i++)
|
|
{
|
|
tick = &(*tickList)[i];
|
|
|
|
if (tick->major)
|
|
{
|
|
if (tick->textSize > 0)
|
|
{
|
|
float x = tick->pos / m_frequencyScale.getSize();
|
|
q3[6*effectiveTicks] = x;
|
|
q3[6*effectiveTicks+1] = 1.0;
|
|
q3[6*effectiveTicks+2] = 0.0;
|
|
q3[6*effectiveTicks+3] = x;
|
|
q3[6*effectiveTicks+4] = 1.0;
|
|
q3[6*effectiveTicks+5] = 1.0;
|
|
effectiveTicks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, (float) m_displayGridIntensity / 100.0f);
|
|
m_glShaderSimple.drawSegments(spectrogramGridMatrix, color, q3, 2*effectiveTicks, 3);
|
|
}
|
|
|
|
glFunctions->glViewport(0, 0, width()*devicePixelRatio, height()*devicePixelRatio);
|
|
}
|
|
|
|
// paint histogram grid
|
|
if ((m_displayHistogram || m_displayMaxHold || m_displayCurrent) && (m_displayGrid))
|
|
{
|
|
const ScaleEngine::TickList* tickList;
|
|
const ScaleEngine::Tick* tick;
|
|
tickList = &m_powerScale.getTickList();
|
|
|
|
{
|
|
GLfloat *q3 = m_q3TickPower.m_array;
|
|
int effectiveTicks = 0;
|
|
|
|
for (int i= 0; i < tickList->count(); i++)
|
|
{
|
|
tick = &(*tickList)[i];
|
|
|
|
if (tick->major)
|
|
{
|
|
if (tick->textSize > 0)
|
|
{
|
|
float y = tick->pos / m_powerScale.getSize();
|
|
q3[4*effectiveTicks] = 0;
|
|
q3[4*effectiveTicks+1] = 1-y;
|
|
q3[4*effectiveTicks+2] = 1;
|
|
q3[4*effectiveTicks+3] = 1-y;
|
|
effectiveTicks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, (float) m_displayGridIntensity / 100.0f);
|
|
m_glShaderSimple.drawSegments(m_glHistogramBoxMatrix, color, q3, 2*effectiveTicks);
|
|
}
|
|
|
|
tickList = &m_frequencyScale.getTickList();
|
|
|
|
{
|
|
GLfloat *q3 = m_q3TickFrequency.m_array;
|
|
int effectiveTicks = 0;
|
|
|
|
for (int i= 0; i < tickList->count(); i++)
|
|
{
|
|
tick = &(*tickList)[i];
|
|
|
|
if (tick->major)
|
|
{
|
|
if (tick->textSize > 0)
|
|
{
|
|
float x = tick->pos / m_frequencyScale.getSize();
|
|
q3[4*effectiveTicks] = x;
|
|
q3[4*effectiveTicks+1] = 0;
|
|
q3[4*effectiveTicks+2] = x;
|
|
q3[4*effectiveTicks+3] = 1;
|
|
effectiveTicks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, (float) m_displayGridIntensity / 100.0f);
|
|
m_glShaderSimple.drawSegments(m_glHistogramBoxMatrix, color, q3, 2*effectiveTicks);
|
|
}
|
|
}
|
|
|
|
// paint rect around histogram (do last, so on top of filled spectrum)
|
|
if (m_displayHistogram || m_displayMaxHold || m_displayCurrent)
|
|
{
|
|
{
|
|
GLfloat q3[] {
|
|
1, 1,
|
|
0, 1,
|
|
0, 0,
|
|
1, 0
|
|
};
|
|
|
|
QVector4D color(1.0f, 1.0f, 1.0f, 0.5f);
|
|
m_glShaderSimple.drawContour(m_glHistogramBoxMatrix, color, q3, 4);
|
|
}
|
|
}
|
|
|
|
// Paint info line
|
|
{
|
|
GLfloat vtx1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
GLfloat tex1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
|
|
m_glShaderInfo.drawSurface(m_glInfoBoxMatrix, tex1, vtx1, 4);
|
|
}
|
|
|
|
if (m_displayCursorStats || m_displayPeakStats) {
|
|
paintStatusLineRight();
|
|
}
|
|
|
|
if (m_currentSpectrum) {
|
|
measure(&m_currentSpectrum[m_fftMin], true);
|
|
}
|
|
|
|
m_mutex.unlock();
|
|
|
|
#ifdef ENABLE_PROFILER
|
|
if (m_profileName.isEmpty())
|
|
{
|
|
// Try to use the window name for the profile name
|
|
QString windowTitle;
|
|
for (QWidget *widget = parentWidget(); widget != nullptr; widget = widget->parentWidget())
|
|
{
|
|
windowTitle = widget->windowTitle();
|
|
if (!windowTitle.isEmpty()) {
|
|
break;
|
|
}
|
|
}
|
|
// Add this address so we get per-spectrum profile data
|
|
if (windowTitle.isEmpty()) {
|
|
m_profileName = QString("Spectrum @%1").arg((quint64)this, 0, 16);
|
|
} else {
|
|
m_profileName = QString("%1 @%2").arg(windowTitle).arg((quint64)this, 0, 16);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
PROFILER_STOP(m_profileName)
|
|
} // paintGL
|
|
|
|
// Highlight power band for SFDR
|
|
void GLSpectrumView::drawPowerBandMarkers(float max, float min, const QVector4D &color)
|
|
{
|
|
float p1 = (m_powerScale.getRangeMax() - min) / m_powerScale.getRange();
|
|
float p2 = (m_powerScale.getRangeMax() - max) / m_powerScale.getRange();
|
|
|
|
GLfloat q3[] {
|
|
1, p2,
|
|
0, p2,
|
|
0, p1,
|
|
1, p1,
|
|
0, p1,
|
|
0, p2
|
|
};
|
|
|
|
m_glShaderSimple.drawSurface(m_glHistogramBoxMatrix, color, q3, 4);
|
|
}
|
|
|
|
// Highlight bandwidth being measured
|
|
void GLSpectrumView::drawBandwidthMarkers(int64_t centerFrequency, int bandwidth, const QVector4D &color)
|
|
{
|
|
float f1 = (centerFrequency - bandwidth / 2);
|
|
float f2 = (centerFrequency + bandwidth / 2);
|
|
float x1 = (f1 - m_frequencyScale.getRangeMin()) / m_frequencyScale.getRange();
|
|
float x2 = (f2 - m_frequencyScale.getRangeMin()) / m_frequencyScale.getRange();
|
|
|
|
GLfloat q3[] {
|
|
x2, 1,
|
|
x1, 1,
|
|
x1, 0,
|
|
x2, 0,
|
|
x1, 0,
|
|
x1, 1
|
|
};
|
|
|
|
m_glShaderSimple.drawSurface(m_glHistogramBoxMatrix, color, q3, 4);
|
|
}
|
|
|
|
// Highlight peak being measured. Note that the peak isn't always at the center
|
|
void GLSpectrumView::drawPeakMarkers(int64_t startFrequency, int64_t endFrequency, const QVector4D &color)
|
|
{
|
|
float x1 = (startFrequency - m_frequencyScale.getRangeMin()) / m_frequencyScale.getRange();
|
|
float x2 = (endFrequency - m_frequencyScale.getRangeMin()) / m_frequencyScale.getRange();
|
|
|
|
GLfloat q3[] {
|
|
x2, 1,
|
|
x1, 1,
|
|
x1, 0,
|
|
x2, 0,
|
|
x1, 0,
|
|
x1, 1
|
|
};
|
|
|
|
m_glShaderSimple.drawSurface(m_glHistogramBoxMatrix, color, q3, 4);
|
|
}
|
|
|
|
void GLSpectrumView::drawSpectrumMarkers()
|
|
{
|
|
if (!m_currentSpectrum) {
|
|
return;
|
|
}
|
|
|
|
QVector4D lineColor(1.0f, 1.0f, 1.0f, 0.3f);
|
|
|
|
// paint histogram markers
|
|
if (m_histogramMarkers.size() > 0)
|
|
{
|
|
for (int i = 0; i < m_histogramMarkers.size(); i++)
|
|
{
|
|
if (!m_histogramMarkers.at(i).m_show) {
|
|
continue;
|
|
}
|
|
|
|
QPointF ypoint = m_histogramMarkers.at(i).m_point;
|
|
QString powerStr = m_histogramMarkers.at(i).m_powerStr;
|
|
|
|
if (m_histogramMarkers.at(i).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePower)
|
|
{
|
|
float power = m_linear ?
|
|
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 ?
|
|
0 :
|
|
ypoint.ry() > 1 ? 1 : ypoint.ry();
|
|
powerStr = displayPower(
|
|
power,
|
|
m_linear ? 'e' : 'f',
|
|
m_linear ? 3 : 1
|
|
);
|
|
}
|
|
else if (m_histogramMarkers.at(i).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePowerMax)
|
|
{
|
|
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))
|
|
{
|
|
m_histogramMarkers[i].m_powerMax = power;
|
|
m_histogramMarkers[i].m_holdReset = false;
|
|
}
|
|
|
|
float powerMax = m_linear ?
|
|
m_histogramMarkers[i].m_powerMax * (m_useCalibration ? m_calibrationGain : 1.0f) :
|
|
m_histogramMarkers[i].m_powerMax + (m_useCalibration ? m_calibrationShiftdB : 0.0f);
|
|
|
|
ypoint.ry() =
|
|
(m_powerScale.getRangeMax() - powerMax) / m_powerScale.getRange();
|
|
ypoint.ry() = ypoint.ry() < 0 ?
|
|
0 : ypoint.ry() > 1 ?
|
|
1 : ypoint.ry();
|
|
powerStr = displayPower(
|
|
powerMax,
|
|
m_linear ? 'e' : 'f',
|
|
m_linear ? 3 : 1
|
|
);
|
|
}
|
|
|
|
// crosshairs
|
|
GLfloat h[] {
|
|
(float) m_histogramMarkers.at(i).m_point.x(), 0,
|
|
(float) m_histogramMarkers.at(i).m_point.x(), 1
|
|
};
|
|
m_glShaderSimple.drawSegments(m_glHistogramBoxMatrix, lineColor, h, 2);
|
|
GLfloat v[] {
|
|
0, (float) ypoint.y(),
|
|
1, (float) ypoint.y()
|
|
};
|
|
m_glShaderSimple.drawSegments(m_glHistogramBoxMatrix, lineColor, v, 2);
|
|
QColor textColor = m_histogramMarkers.at(i).m_markerColor;
|
|
// text
|
|
if (i == 0)
|
|
{
|
|
drawTextOverlay(
|
|
m_histogramMarkers.at(i).m_frequencyStr,
|
|
textColor,
|
|
m_textOverlayFont,
|
|
m_histogramMarkers.at(i).m_point.x() * m_histogramRect.width(),
|
|
(m_invertedWaterfall || (m_waterfallHeight == 0)) ? m_histogramRect.height() : 0,
|
|
m_histogramMarkers.at(i).m_point.x() < 0.5f,
|
|
!m_invertedWaterfall && (m_waterfallHeight != 0),
|
|
m_histogramRect);
|
|
drawTextOverlay(
|
|
powerStr,
|
|
textColor,
|
|
m_textOverlayFont,
|
|
0,
|
|
ypoint.y() * m_histogramRect.height(),
|
|
true,
|
|
ypoint.y() < 0.5f,
|
|
m_histogramRect);
|
|
}
|
|
else
|
|
{
|
|
textColor.setAlpha(192);
|
|
float power0, poweri;
|
|
|
|
if (m_histogramMarkers.at(0).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePower) {
|
|
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 {
|
|
power0 = m_linear ? m_histogramMarkers.at(0).m_power : CalcDb::dbPower(m_histogramMarkers.at(0).m_power);
|
|
}
|
|
|
|
if (m_histogramMarkers.at(i).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePower) {
|
|
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 {
|
|
poweri = m_linear ? m_histogramMarkers.at(i).m_power : CalcDb::dbPower(m_histogramMarkers.at(i).m_power);
|
|
}
|
|
|
|
QString deltaPowerStr;
|
|
|
|
if (m_linear) {
|
|
deltaPowerStr = QString::number(poweri - power0, 'e', 3);
|
|
} else {
|
|
deltaPowerStr = QString::number(poweri - power0, 'f', 1);
|
|
}
|
|
|
|
drawTextOverlay(
|
|
m_histogramMarkers.at(i).m_deltaFrequencyStr,
|
|
textColor,
|
|
m_textOverlayFont,
|
|
m_histogramMarkers.at(i).m_point.x() * m_histogramRect.width(),
|
|
(m_invertedWaterfall || (m_waterfallHeight == 0)) ? 0 : m_histogramRect.height(),
|
|
m_histogramMarkers.at(i).m_point.x() < 0.5f,
|
|
(m_invertedWaterfall || (m_waterfallHeight == 0)),
|
|
m_histogramRect);
|
|
drawTextOverlay(
|
|
deltaPowerStr,
|
|
textColor,
|
|
m_textOverlayFont,
|
|
m_histogramRect.width(),
|
|
ypoint.y() * m_histogramRect.height(),
|
|
false,
|
|
ypoint.y() < 0.5f,
|
|
m_histogramRect);
|
|
}
|
|
}
|
|
}
|
|
|
|
// paint waterfall markers
|
|
if (m_waterfallMarkers.size() > 0)
|
|
{
|
|
// crosshairs
|
|
for (int i = 0; i < m_waterfallMarkers.size(); i++)
|
|
{
|
|
if (!m_waterfallMarkers.at(i).m_show) {
|
|
continue;
|
|
}
|
|
|
|
GLfloat h[] {
|
|
(float) m_waterfallMarkers.at(i).m_point.x(), 0,
|
|
(float) m_waterfallMarkers.at(i).m_point.x(), 1
|
|
};
|
|
m_glShaderSimple.drawSegments(m_glWaterfallBoxMatrix, lineColor, h, 2);
|
|
GLfloat v[] {
|
|
0, (float) m_waterfallMarkers.at(i).m_point.y(),
|
|
1, (float) m_waterfallMarkers.at(i).m_point.y()
|
|
};
|
|
m_glShaderSimple.drawSegments(m_glWaterfallBoxMatrix, lineColor, v, 2);
|
|
// }
|
|
// text
|
|
// for (int i = 0; i < m_waterfallMarkers.size(); i++)
|
|
// {
|
|
QColor textColor = m_waterfallMarkers.at(i).m_markerColor;
|
|
textColor.setAlpha(192);
|
|
|
|
if (i == 0)
|
|
{
|
|
drawTextOverlay(
|
|
m_waterfallMarkers.at(i).m_frequencyStr,
|
|
textColor,
|
|
m_textOverlayFont,
|
|
m_waterfallMarkers.at(i).m_point.x() * m_waterfallRect.width(),
|
|
(!m_invertedWaterfall || (m_histogramHeight == 0)) ? m_waterfallRect.height() : 0,
|
|
m_waterfallMarkers.at(i).m_point.x() < 0.5f,
|
|
m_invertedWaterfall && (m_histogramHeight != 0),
|
|
m_waterfallRect);
|
|
drawTextOverlay(
|
|
m_waterfallMarkers.at(i).m_timeStr,
|
|
textColor,
|
|
m_textOverlayFont,
|
|
0,
|
|
m_waterfallMarkers.at(i).m_point.y() * m_waterfallRect.height(),
|
|
true,
|
|
m_waterfallMarkers.at(i).m_point.y() < 0.5f,
|
|
m_waterfallRect);
|
|
}
|
|
else
|
|
{
|
|
drawTextOverlay(
|
|
m_waterfallMarkers.at(i).m_deltaFrequencyStr,
|
|
textColor,
|
|
m_textOverlayFont,
|
|
m_waterfallMarkers.at(i).m_point.x() * m_waterfallRect.width(),
|
|
(!m_invertedWaterfall || (m_histogramHeight == 0)) ? 0 : m_waterfallRect.height(),
|
|
m_waterfallMarkers.at(i).m_point.x() < 0.5f,
|
|
!m_invertedWaterfall || (m_histogramHeight == 0),
|
|
m_waterfallRect);
|
|
drawTextOverlay(
|
|
m_waterfallMarkers.at(i).m_deltaTimeStr,
|
|
textColor,
|
|
m_textOverlayFont,
|
|
m_waterfallRect.width(),
|
|
m_waterfallMarkers.at(i).m_point.y() * m_waterfallRect.height(),
|
|
false,
|
|
m_waterfallMarkers.at(i).m_point.y() < 0.5f,
|
|
m_waterfallRect);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::drawAnnotationMarkers()
|
|
{
|
|
if ((!m_currentSpectrum) || (m_visibleAnnotationMarkers.size() == 0)) {
|
|
return;
|
|
}
|
|
|
|
float h = m_annotationMarkerHeight / (float) m_histogramHeight;
|
|
float htop = 1.0f / (float) m_histogramHeight;
|
|
|
|
for (const auto &marker : m_visibleAnnotationMarkers)
|
|
{
|
|
if (marker->m_show == SpectrumAnnotationMarker::Hidden) {
|
|
continue;
|
|
}
|
|
|
|
QVector4D color(marker->m_markerColor.redF(), marker->m_markerColor.greenF(), marker->m_markerColor.blueF(), 0.5f);
|
|
|
|
if (marker->m_bandwidth == 0)
|
|
{
|
|
GLfloat d[] {
|
|
marker->m_startPos, htop,
|
|
marker->m_startPos, h
|
|
};
|
|
m_glShaderSimple.drawSegments(m_glHistogramBoxMatrix, color, d, 2);
|
|
}
|
|
else
|
|
{
|
|
GLfloat q3[] {
|
|
marker->m_stopPos, h,
|
|
marker->m_startPos, h,
|
|
marker->m_startPos, htop,
|
|
marker->m_stopPos, htop
|
|
};
|
|
m_glShaderSimple.drawSurface(m_glHistogramBoxMatrix, color, q3, 4);
|
|
}
|
|
|
|
// Always draw a line in the top area, so we can see where bands start/stop when contiguous
|
|
// When show is ShowFull, we draw at full height of spectrum
|
|
bool full = marker->m_show == SpectrumAnnotationMarker::ShowFull;
|
|
|
|
GLfloat d1[] {
|
|
marker->m_startPos, full ? 0 : htop,
|
|
marker->m_startPos, full ? 1 : h,
|
|
};
|
|
m_glShaderSimple.drawSegments(m_glHistogramBoxMatrix, color, d1, 2);
|
|
|
|
if (marker->m_bandwidth != 0)
|
|
{
|
|
GLfloat d2[] {
|
|
marker->m_stopPos, full ? 0 : htop,
|
|
marker->m_stopPos, full ? 1 : h,
|
|
};
|
|
m_glShaderSimple.drawSegments(m_glHistogramBoxMatrix, color, d2, 2);
|
|
}
|
|
|
|
if ((marker->m_show == SpectrumAnnotationMarker::ShowFull) || (marker->m_show == SpectrumAnnotationMarker::ShowText))
|
|
{
|
|
float txtpos = marker->m_startPos < 0.5f ?
|
|
marker->m_startPos :
|
|
marker->m_stopPos;
|
|
|
|
drawTextOverlay(
|
|
marker->m_text,
|
|
QColor(255, 255, 255, 192),
|
|
m_textOverlayFont,
|
|
txtpos * m_histogramRect.width(),
|
|
0,
|
|
marker->m_startPos < 0.5f,
|
|
true,
|
|
m_histogramRect);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find and display peaks
|
|
void GLSpectrumView::measurePeaks(const Real *spectrum)
|
|
{
|
|
// Copy current spectrum so we can modify it
|
|
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(spectrumCopy);
|
|
int left, right;
|
|
peakWidth(spectrumCopy, peakBin, left, right, 0, m_nbBins);
|
|
left++;
|
|
right--;
|
|
|
|
float power = m_linear ?
|
|
spectrumCopy[peakBin] * (m_useCalibration ? m_calibrationGain : 1.0f) :
|
|
spectrumCopy[peakBin] + (m_useCalibration ? m_calibrationShiftdB : 0.0f);
|
|
int64_t frequency = binToFrequency(peakBin);
|
|
|
|
// Add to table
|
|
if (m_measurements) {
|
|
m_measurements->setPeak(i, frequency, power);
|
|
}
|
|
|
|
if (m_measurementHighlight)
|
|
{
|
|
float x = peakBin / (float)m_nbBins;
|
|
float y = (m_powerScale.getRangeMax() - power) / m_powerScale.getRange();
|
|
|
|
QString text = QString::number(i + 1);
|
|
|
|
drawTextOverlayCentered(
|
|
text,
|
|
QColor(255, 255, 255),
|
|
m_textOverlayFont,
|
|
x * m_histogramRect.width(),
|
|
y * m_histogramRect.height(),
|
|
m_histogramRect);
|
|
}
|
|
|
|
// Remove peak from spectrum so not found on next pass
|
|
for (int j = left; j <= right; j++) {
|
|
spectrumCopy[j] = -std::numeric_limits<float>::max();
|
|
}
|
|
}
|
|
|
|
delete[] spectrumCopy;
|
|
}
|
|
|
|
// Calculate and display channel power
|
|
void GLSpectrumView::measureChannelPower(const Real *spectrum, bool updateGUI)
|
|
{
|
|
float power;
|
|
qint64 centerFrequency = getDisplayedCenterFrequency();
|
|
|
|
power = calcChannelPower(spectrum, centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth);
|
|
if (m_measurements) {
|
|
m_measurements->setChannelPower(power, updateGUI);
|
|
}
|
|
if (m_measurementHighlight && updateGUI) {
|
|
drawBandwidthMarkers(centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth, m_measurementLightMarkerColor);
|
|
}
|
|
}
|
|
|
|
// Calculate and display channel power and adjacent channel power
|
|
void GLSpectrumView::measureAdjacentChannelPower(const Real *spectrum, bool updateGUI)
|
|
{
|
|
float power, powerLeft, powerRight;
|
|
qint64 centerFrequency = getDisplayedCenterFrequency();
|
|
|
|
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, updateGUI);
|
|
}
|
|
if (m_measurementHighlight && updateGUI)
|
|
{
|
|
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(const Real *spectrum, bool updateGUI)
|
|
{
|
|
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;
|
|
int idx = start;
|
|
float gain = m_useCalibration ? m_calibrationGain : 1.0f;
|
|
float shift = m_useCalibration ? m_calibrationShiftdB : 0.0f;
|
|
|
|
totalPower = CalcDb::powerFromdB(calcChannelPower(spectrum, centerFrequency + m_measurementCenterFrequencyOffset, m_measurementBandwidth));
|
|
do
|
|
{
|
|
if ((idx >= 0) && (idx < m_nbBins))
|
|
{
|
|
if (m_linear) {
|
|
power += spectrum[idx] * gain;
|
|
} else {
|
|
power += CalcDb::powerFromdB(spectrum[idx]) + shift;
|
|
}
|
|
width++;
|
|
}
|
|
|
|
step++;
|
|
if ((step & 1) == 1) {
|
|
idx -= step;
|
|
} else {
|
|
idx += step;
|
|
}
|
|
}
|
|
while (((power / totalPower) < 0.99f) && (step < m_nbBins));
|
|
|
|
float occupiedBandwidth = width * hzPerBin;
|
|
if (m_measurements) {
|
|
m_measurements->setOccupiedBandwidth(occupiedBandwidth, updateGUI);
|
|
}
|
|
if (m_measurementHighlight && updateGUI)
|
|
{
|
|
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(const Real *spectrum, bool updateGUI)
|
|
{
|
|
// Find max peak and it's power in dB
|
|
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(spectrum[i]) : spectrum[i];
|
|
if (peakPower - power > 3.0f)
|
|
{
|
|
rightBin = i - 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Search left until 3dB from peak
|
|
int leftBin = peakBin;
|
|
for (int i = peakBin - 1; i >= 0; i--)
|
|
{
|
|
float power = m_linear ? CalcDb::dbPower(spectrum[i]) : spectrum[i];
|
|
if (peakPower - power > 3.0f)
|
|
{
|
|
leftBin = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Calculate bandwidth
|
|
int bins = rightBin - leftBin - 1;
|
|
bins = std::max(1, bins);
|
|
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, updateGUI);
|
|
}
|
|
if (m_measurementHighlight && updateGUI) {
|
|
drawBandwidthMarkers(centerFrequency, bandwidth, m_measurementLightMarkerColor);
|
|
}
|
|
}
|
|
|
|
const QVector4D GLSpectrumView::m_measurementLightMarkerColor = QVector4D(0.6f, 0.6f, 0.6f, 0.2f);
|
|
const QVector4D GLSpectrumView::m_measurementDarkMarkerColor = QVector4D(0.6f, 0.6f, 0.6f, 0.15f);
|
|
|
|
// Find the width of a peak, by seaching in either direction until
|
|
// power is no longer falling
|
|
void GLSpectrumView::peakWidth(const Real *spectrum, int center, int &left, int &right, int maxLeft, int maxRight) const
|
|
{
|
|
float prevLeft = spectrum[center];
|
|
float prevRight = spectrum[center];
|
|
left = center - 1;
|
|
right = center + 1;
|
|
// Use <= as SSB spectrums have duplicated values
|
|
while ((left > maxLeft) && (spectrum[left] <= prevLeft) && (right < maxRight) && (spectrum[right] <= prevRight))
|
|
{
|
|
prevLeft = spectrum[left];
|
|
left--;
|
|
prevRight = spectrum[right];
|
|
right++;
|
|
}
|
|
}
|
|
|
|
int GLSpectrumView::findPeakBin(const Real *spectrum) const
|
|
{
|
|
int bin;
|
|
float power;
|
|
|
|
bin = 0;
|
|
power = spectrum[0];
|
|
for (int i = 1; i < m_nbBins; i++)
|
|
{
|
|
if (spectrum[i] > power)
|
|
{
|
|
power = spectrum[i];
|
|
bin = i;
|
|
}
|
|
}
|
|
return bin;
|
|
}
|
|
|
|
float GLSpectrumView::calPower(float power) const
|
|
{
|
|
if (m_linear) {
|
|
return power * (m_useCalibration ? m_calibrationGain : 1.0f);
|
|
} else {
|
|
return CalcDb::powerFromdB(power) + (m_useCalibration ? m_calibrationShiftdB : 0.0f);
|
|
}
|
|
}
|
|
|
|
int GLSpectrumView::frequencyToBin(int64_t frequency) const
|
|
{
|
|
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 ? (getDisplayedSampleRate()/2) : getDisplayedSampleRate()) / (float)m_fftSize;
|
|
return m_frequencyScale.getRangeMin() + bin * rbw;
|
|
}
|
|
|
|
// Find a peak and measure SNR / THD / SINAD
|
|
void GLSpectrumView::measureSNR(const Real *spectrum, bool updateGUI)
|
|
{
|
|
// Find bin with max peak - that will be our signal
|
|
int sig = findPeakBin(spectrum);
|
|
int sigLeft, sigRight;
|
|
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 && updateGUI) {
|
|
drawPeakMarkers(binToFrequency(sigLeft+1), binToFrequency(sigRight-1), m_measurementLightMarkerColor);
|
|
}
|
|
|
|
// Find the harmonics and highlight them
|
|
QList<int> hBinsLeft;
|
|
QList<int> hBinsRight;
|
|
QList<int> hBinsBins;
|
|
for (int h = 2; h < m_measurementHarmonics + 2; h++)
|
|
{
|
|
float hFreq = sigFreq * h;
|
|
if (hFreq < m_frequencyScale.getRangeMax())
|
|
{
|
|
int hBin = frequencyToBin(hFreq);
|
|
// Check if peak is an adjacent bin
|
|
if (spectrum[hBin-1] > spectrum[hBin]) {
|
|
hBin--;
|
|
} else if (spectrum[hBin+1] > spectrum[hBin]) {
|
|
hBin++;
|
|
}
|
|
hFreq = binToFrequency(hBin);
|
|
int hLeft, hRight;
|
|
peakWidth(spectrum, hBin, hLeft, hRight, hBin - binsLeft, hBin + binsRight);
|
|
int hBins = hRight - hLeft - 1;
|
|
if (m_measurementHighlight && updateGUI) {
|
|
drawPeakMarkers(binToFrequency(hLeft+1), binToFrequency(hRight-1), m_measurementDarkMarkerColor);
|
|
}
|
|
hBinsLeft.append(hLeft);
|
|
hBinsRight.append(hRight);
|
|
hBinsBins.append(hBins);
|
|
}
|
|
}
|
|
|
|
// Integrate signal, harmonic and noise power
|
|
float sigPower = 0.0f;
|
|
float noisePower = 0.0f;
|
|
float harmonicPower = 0.0f;
|
|
QList<float> noise;
|
|
float gain = m_useCalibration ? m_calibrationGain : 1.0f;
|
|
float shift = m_useCalibration ? m_calibrationShiftdB : 0.0f;
|
|
|
|
for (int i = 0; i < m_nbBins; i++)
|
|
{
|
|
float power;
|
|
if (m_linear) {
|
|
power = spectrum[i] * gain;
|
|
} else {
|
|
power = CalcDb::powerFromdB(spectrum[i]) + shift;
|
|
}
|
|
|
|
// Signal power
|
|
if ((i > sigLeft) && (i < sigRight))
|
|
{
|
|
sigPower += power;
|
|
continue;
|
|
}
|
|
|
|
// Harmonics
|
|
for (int h = 0; h < hBinsLeft.size(); h++)
|
|
{
|
|
if ((i > hBinsLeft[h]) && (i < hBinsRight[h]))
|
|
{
|
|
harmonicPower += power;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Noise
|
|
noisePower += power;
|
|
noise.append(power);
|
|
}
|
|
|
|
// Calculate median of noise
|
|
float noiseMedian = 0.0;
|
|
if (noise.size() > 0)
|
|
{
|
|
auto m = noise.begin() + noise.size()/2;
|
|
std::nth_element(noise.begin(), m, noise.end());
|
|
noiseMedian = noise[noise.size()/2];
|
|
}
|
|
|
|
// Assume we have similar noise where the signal and harmonics are
|
|
float inBandNoise = noiseMedian * sigBins;
|
|
noisePower += inBandNoise;
|
|
sigPower -= inBandNoise;
|
|
for (auto hBins : hBinsBins)
|
|
{
|
|
float hNoise = noiseMedian * hBins;
|
|
noisePower += hNoise;
|
|
harmonicPower -= hNoise;
|
|
}
|
|
|
|
if (m_measurements)
|
|
{
|
|
// Calculate SNR in dB over full bandwidth
|
|
float snr = CalcDb::dbPower(sigPower / noisePower);
|
|
|
|
// Calculate SNR, where noise is median of noise summed over signal b/w
|
|
float snfr = CalcDb::dbPower(sigPower / inBandNoise);
|
|
|
|
// Calculate THD - Total harmonic distortion
|
|
float thd = harmonicPower / sigPower;
|
|
float thdDB = CalcDb::dbPower(thd);
|
|
|
|
// Calculate THD+N - Total harmonic distortion plus noise
|
|
float thdpn = CalcDb::dbPower((harmonicPower + noisePower) / sigPower);
|
|
|
|
// 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, updateGUI);
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::measureSFDR(const Real *spectrum, bool updateGUI)
|
|
{
|
|
// Find first peak which is our signal
|
|
int peakBin = findPeakBin(spectrum);
|
|
int peakLeft, peakRight;
|
|
peakWidth(spectrum, peakBin, peakLeft, peakRight, 0, m_nbBins);
|
|
|
|
// Find next largest peak, which is the spur
|
|
int nextPeakBin = -1;
|
|
float nextPeakPower = -std::numeric_limits<float>::max();
|
|
for (int i = 0; i < m_nbBins; i++)
|
|
{
|
|
if ((i < peakLeft) || (i > peakRight))
|
|
{
|
|
if (spectrum[i] > nextPeakPower)
|
|
{
|
|
nextPeakBin = i;
|
|
nextPeakPower = spectrum[i];
|
|
}
|
|
}
|
|
}
|
|
if (nextPeakBin != -1)
|
|
{
|
|
// Calculate SFDR in dB from difference between two peaks
|
|
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, updateGUI);
|
|
}
|
|
if (m_measurementHighlight && updateGUI)
|
|
{
|
|
if (m_linear) {
|
|
drawPowerBandMarkers(peakPower, nextPeakPower, m_measurementDarkMarkerColor);
|
|
} else {
|
|
drawPowerBandMarkers(peakPowerDB, nextPeakPowerDB, m_measurementDarkMarkerColor);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find power and frequency of max peak in current spectrum
|
|
void GLSpectrumView::findPeak(const Real *spectrum, float &power, float &frequency) const
|
|
{
|
|
int bin;
|
|
|
|
bin = 0;
|
|
power = spectrum[0];
|
|
for (int i = 1; i < m_nbBins; i++)
|
|
{
|
|
if (spectrum[i] > power)
|
|
{
|
|
power = spectrum[i];
|
|
bin = i;
|
|
}
|
|
}
|
|
|
|
power = m_linear ?
|
|
power * (m_useCalibration ? m_calibrationGain : 1.0f) :
|
|
power + (m_useCalibration ? m_calibrationShiftdB : 0.0f);
|
|
frequency = binToFrequency(bin);
|
|
}
|
|
|
|
// Calculate channel power in dB
|
|
float GLSpectrumView::calcChannelPower(const Real *spectrum, int64_t centerFrequency, int channelBandwidth) const
|
|
{
|
|
float hzPerBin = getDisplayedSampleRate() / (float) m_fftSize;
|
|
int bins = channelBandwidth / hzPerBin;
|
|
int start = frequencyToBin(centerFrequency) - (bins / 2);
|
|
int end = start + bins;
|
|
float power = 0.0;
|
|
|
|
start = std::max(start, 0);
|
|
end = std::min(end, m_nbBins);
|
|
|
|
if (m_linear)
|
|
{
|
|
float gain = m_useCalibration ? m_calibrationGain : 1.0f;
|
|
for (int i = start; i < end; i++) {
|
|
power += spectrum[i] * gain;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float shift = m_useCalibration ? m_calibrationShiftdB : 0.0f;
|
|
for (int i = start; i < end; i++) {
|
|
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() < 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)
|
|
{
|
|
if ((m_cursorState == CSSplitterMoving) || (m_cursorState == CSChannelMoving)) {
|
|
releaseMouse();
|
|
}
|
|
|
|
setCursor(Qt::ArrowCursor);
|
|
m_cursorState = CSNormal;
|
|
}
|
|
}
|
|
|
|
// 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 < 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;
|
|
m_bottomMargin = fm.ascent() * 1;
|
|
m_infoHeight = fm.height() * 3;
|
|
|
|
m_waterfallTop = 0;
|
|
m_frequencyScaleHeight = fm.height() * 3; // +1 line for marker frequency scale
|
|
int frequencyScaleTop = 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))
|
|
{
|
|
m_waterfallHeight = height() * m_waterfallShare - 1;
|
|
|
|
if (m_waterfallHeight < 0) {
|
|
m_waterfallHeight = 0;
|
|
}
|
|
|
|
if (m_invertedWaterfall)
|
|
{
|
|
m_histogramTop = m_topMargin;
|
|
m_histogramHeight = height() - m_topMargin - m_waterfallHeight - m_frequencyScaleHeight - m_bottomMargin;
|
|
m_waterfallTop = m_histogramTop + m_histogramHeight + m_frequencyScaleHeight + 1;
|
|
frequencyScaleTop = m_histogramTop + m_histogramHeight + 1;
|
|
}
|
|
else
|
|
{
|
|
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);
|
|
setTimeScaleRange();
|
|
|
|
m_leftMargin = m_timeScale.getScaleWidth();
|
|
|
|
setPowerScale(m_histogramHeight);
|
|
|
|
m_leftMargin += 2 * M;
|
|
|
|
setFrequencyScale();
|
|
|
|
m_glWaterfallBoxMatrix.setToIdentity();
|
|
m_glWaterfallBoxMatrix.translate(
|
|
-1.0f + ((float)(2*m_leftMargin) / (float) width()),
|
|
1.0f - ((float)(2*m_waterfallTop) / (float) height())
|
|
);
|
|
m_glWaterfallBoxMatrix.scale(
|
|
((float) 2 * (width() - m_leftMargin - m_rightMargin)) / (float) width(),
|
|
(float) (-2*m_waterfallHeight) / (float) height()
|
|
);
|
|
|
|
m_glHistogramBoxMatrix.setToIdentity();
|
|
m_glHistogramBoxMatrix.translate(
|
|
-1.0f + ((float)(2*m_leftMargin) / (float) width()),
|
|
1.0f - ((float)(2*m_histogramTop) / (float) height())
|
|
);
|
|
m_glHistogramBoxMatrix.scale(
|
|
((float) 2 * (width() - m_leftMargin - m_rightMargin)) / (float) width(),
|
|
(float) (-2*m_histogramHeight) / (float) height()
|
|
);
|
|
|
|
m_glHistogramSpectrumMatrix.setToIdentity();
|
|
m_glHistogramSpectrumMatrix.translate(
|
|
-1.0f + ((float)(2*m_leftMargin) / (float) width()),
|
|
1.0f - ((float)(2*m_histogramTop) / (float) height())
|
|
);
|
|
m_glHistogramSpectrumMatrix.scale(
|
|
((float) 2 * (width() - m_leftMargin - m_rightMargin)) / ((float) width() * (float)(m_nbBins)),
|
|
((float) 2*m_histogramHeight / height()) / m_powerRange
|
|
);
|
|
|
|
// m_frequencyScaleRect = QRect(
|
|
// 0,
|
|
// frequencyScaleTop,
|
|
// width(),
|
|
// m_frequencyScaleHeight
|
|
// );
|
|
|
|
m_glFrequencyScaleBoxMatrix.setToIdentity();
|
|
m_glFrequencyScaleBoxMatrix.translate (
|
|
-1.0f,
|
|
1.0f - ((float) 2*frequencyScaleTop / (float) height())
|
|
);
|
|
m_glFrequencyScaleBoxMatrix.scale (
|
|
2.0f,
|
|
(float) -2*m_frequencyScaleHeight / (float) height()
|
|
);
|
|
|
|
m_glLeftScaleBoxMatrix.setToIdentity();
|
|
m_glLeftScaleBoxMatrix.translate(-1.0f, 1.0f);
|
|
m_glLeftScaleBoxMatrix.scale(
|
|
(float)(2*(m_leftMargin - 1)) / (float) width(),
|
|
-2.0f
|
|
);
|
|
}
|
|
// displays waterfall/3D spectrogram only
|
|
else if (m_displayWaterfall || m_display3DSpectrogram)
|
|
{
|
|
m_histogramHeight = 0;
|
|
m_histogramTop = 0;
|
|
m_bottomMargin = m_frequencyScaleHeight;
|
|
m_waterfallHeight = height() - m_topMargin - m_frequencyScaleHeight;
|
|
m_waterfallTop = m_topMargin;
|
|
frequencyScaleTop = m_topMargin + m_waterfallHeight + 1;
|
|
|
|
m_timeScale.setSize(m_waterfallHeight);
|
|
setTimeScaleRange();
|
|
|
|
m_leftMargin = m_timeScale.getScaleWidth();
|
|
|
|
setPowerScale((height() - m_topMargin - m_bottomMargin) / 2.0);
|
|
|
|
m_leftMargin += 2 * M;
|
|
|
|
setFrequencyScale();
|
|
|
|
m_glWaterfallBoxMatrix.setToIdentity();
|
|
m_glWaterfallBoxMatrix.translate(
|
|
-1.0f + ((float)(2*m_leftMargin) / (float) width()),
|
|
1.0f - ((float)(2*m_topMargin) / (float) height())
|
|
);
|
|
m_glWaterfallBoxMatrix.scale(
|
|
((float) 2 * (width() - m_leftMargin - m_rightMargin)) / (float) width(),
|
|
(float) (-2*m_waterfallHeight) / (float) height()
|
|
);
|
|
|
|
// m_frequencyScaleRect = QRect(
|
|
// 0,
|
|
// frequencyScaleTop,
|
|
// width(),
|
|
// m_frequencyScaleHeight
|
|
// );
|
|
|
|
m_glFrequencyScaleBoxMatrix.setToIdentity();
|
|
m_glFrequencyScaleBoxMatrix.translate (
|
|
-1.0f,
|
|
1.0f - ((float) 2*frequencyScaleTop / (float) height())
|
|
);
|
|
m_glFrequencyScaleBoxMatrix.scale (
|
|
2.0f,
|
|
(float) -2*m_frequencyScaleHeight / (float) height()
|
|
);
|
|
|
|
m_glLeftScaleBoxMatrix.setToIdentity();
|
|
m_glLeftScaleBoxMatrix.translate(-1.0f, 1.0f);
|
|
m_glLeftScaleBoxMatrix.scale(
|
|
(float)(2*(m_leftMargin - 1)) / (float) width(),
|
|
-2.0f
|
|
);
|
|
}
|
|
// displays histogram only
|
|
else if (m_displayHistogram || m_displayMaxHold || m_displayCurrent)
|
|
{
|
|
m_bottomMargin = m_frequencyScaleHeight;
|
|
frequencyScaleTop = height() - m_bottomMargin;
|
|
m_histogramTop = m_topMargin - 1;
|
|
m_waterfallHeight = 0;
|
|
m_histogramHeight = height() - m_topMargin - m_frequencyScaleHeight;
|
|
|
|
m_leftMargin = 0;
|
|
|
|
setPowerScale(m_histogramHeight);
|
|
|
|
m_leftMargin += 2 * M;
|
|
|
|
setFrequencyScale();
|
|
|
|
m_glHistogramSpectrumMatrix.setToIdentity();
|
|
m_glHistogramSpectrumMatrix.translate(
|
|
-1.0f + ((float)(2*m_leftMargin) / (float) width()),
|
|
1.0f - ((float)(2*m_histogramTop) / (float) height())
|
|
);
|
|
m_glHistogramSpectrumMatrix.scale(
|
|
((float) 2 * (width() - m_leftMargin - m_rightMargin)) / ((float) width() * (float)(m_nbBins)),
|
|
((float) 2*(height() - m_topMargin - m_frequencyScaleHeight)) / (height()*m_powerRange)
|
|
);
|
|
|
|
m_glHistogramBoxMatrix.setToIdentity();
|
|
m_glHistogramBoxMatrix.translate(
|
|
-1.0f + ((float)(2*m_leftMargin) / (float) width()),
|
|
1.0f - ((float)(2*m_histogramTop) / (float) height())
|
|
);
|
|
m_glHistogramBoxMatrix.scale(
|
|
((float) 2 * (width() - m_leftMargin - m_rightMargin)) / (float) width(),
|
|
(float) (-2*(height() - m_topMargin - m_frequencyScaleHeight)) / (float) height()
|
|
);
|
|
|
|
// m_frequencyScaleRect = QRect(
|
|
// 0,
|
|
// frequencyScaleTop,
|
|
// width(),
|
|
// m_frequencyScaleHeight
|
|
// );
|
|
|
|
m_glFrequencyScaleBoxMatrix.setToIdentity();
|
|
m_glFrequencyScaleBoxMatrix.translate (
|
|
-1.0f,
|
|
1.0f - ((float) 2*frequencyScaleTop / (float) height())
|
|
);
|
|
m_glFrequencyScaleBoxMatrix.scale (
|
|
2.0f,
|
|
(float) -2*m_frequencyScaleHeight / (float) height()
|
|
);
|
|
|
|
m_glLeftScaleBoxMatrix.setToIdentity();
|
|
m_glLeftScaleBoxMatrix.translate(-1.0f, 1.0f);
|
|
m_glLeftScaleBoxMatrix.scale(
|
|
(float)(2*(m_leftMargin - 1)) / (float) width(),
|
|
-2.0f
|
|
);
|
|
}
|
|
else
|
|
{
|
|
m_leftMargin = 2;
|
|
m_waterfallHeight = 0;
|
|
}
|
|
|
|
m_glShaderSpectrogram.setScaleX(((width() - m_leftMargin - m_rightMargin) / (float)m_waterfallHeight));
|
|
m_glShaderSpectrogram.setScaleZ((m_histogramHeight != 0 ? m_histogramHeight : m_waterfallHeight / 4) / (float)(width() - m_leftMargin - m_rightMargin));
|
|
|
|
// bounding boxes
|
|
m_frequencyScaleRect = QRect(
|
|
0,
|
|
frequencyScaleTop,
|
|
width(),
|
|
m_frequencyScaleHeight
|
|
);
|
|
|
|
if ((m_invertedWaterfall) || (m_waterfallHeight == 0))
|
|
{
|
|
m_histogramRect = QRectF(
|
|
(float) m_leftMargin / (float) width(),
|
|
(float) m_topMargin / (float) height(),
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) (m_histogramHeight) / (float) height()
|
|
);
|
|
}
|
|
else
|
|
{
|
|
m_histogramRect = QRectF(
|
|
(float) m_leftMargin / (float) width(),
|
|
(float) (m_waterfallTop + m_waterfallHeight + m_frequencyScaleHeight) / (float) height(),
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) m_histogramHeight / (float) height()
|
|
);
|
|
}
|
|
|
|
if (!m_invertedWaterfall || (m_histogramHeight == 0))
|
|
{
|
|
m_waterfallRect = QRectF(
|
|
(float) m_leftMargin / (float) width(),
|
|
(float) m_topMargin / (float) height(),
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) m_waterfallHeight / (float) height()
|
|
);
|
|
}
|
|
else
|
|
{
|
|
m_waterfallRect = QRectF(
|
|
(float) m_leftMargin / (float) width(),
|
|
(float) (m_topMargin + m_histogramHeight + m_frequencyScaleHeight) / (float) height(),
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) (m_waterfallHeight) / (float) height()
|
|
);
|
|
}
|
|
|
|
m_glShaderSpectrogram.setAspectRatio((width() - m_leftMargin - m_rightMargin) / (float)m_waterfallHeight);
|
|
|
|
m_3DSpectrogramBottom = m_bottomMargin;
|
|
if (!m_invertedWaterfall) {
|
|
m_3DSpectrogramBottom += m_histogramHeight + m_frequencyScaleHeight + 1;
|
|
}
|
|
|
|
// channel overlays
|
|
int64_t centerFrequencyUnused;
|
|
int frequencySpan;
|
|
getFrequencyZoom(centerFrequencyUnused, frequencySpan);
|
|
|
|
for (int i = 0; i < m_channelMarkerStates.size(); ++i)
|
|
{
|
|
ChannelMarkerState* dv = m_channelMarkerStates[i];
|
|
|
|
qreal xc, pw, nw, dsbw;
|
|
ChannelMarker::sidebands_t sidebands = dv->m_channelMarker->getSidebands();
|
|
xc = centerFrequency + dv->m_channelMarker->getCenterFrequency(); // marker center frequency
|
|
dsbw = dv->m_channelMarker->getBandwidth();
|
|
|
|
if (sidebands == ChannelMarker::usb)
|
|
{
|
|
nw = dv->m_channelMarker->getLowCutoff() + dv->m_channelMarker->getShift(); // negative bandwidth
|
|
int bw = dv->m_channelMarker->getBandwidth() / 2;
|
|
pw = (qreal) bw + dv->m_channelMarker->getShift(); // positive bandwidth
|
|
}
|
|
else if (sidebands == ChannelMarker::lsb)
|
|
{
|
|
pw = dv->m_channelMarker->getLowCutoff() + dv->m_channelMarker->getShift();
|
|
int bw = dv->m_channelMarker->getBandwidth() / 2;
|
|
nw = (qreal) bw + dv->m_channelMarker->getShift();
|
|
}
|
|
else if (sidebands == ChannelMarker::vusb)
|
|
{
|
|
nw = -dv->m_channelMarker->getOppositeBandwidth() + dv->m_channelMarker->getShift(); // negative bandwidth
|
|
pw = dv->m_channelMarker->getBandwidth() + dv->m_channelMarker->getShift(); // positive bandwidth
|
|
}
|
|
else if (sidebands == ChannelMarker::vlsb) {
|
|
pw = dv->m_channelMarker->getOppositeBandwidth() + dv->m_channelMarker->getShift(); // positive bandwidth
|
|
nw = -dv->m_channelMarker->getBandwidth() + dv->m_channelMarker->getShift(); // negative bandwidth
|
|
}
|
|
else
|
|
{
|
|
pw = (dsbw / 2) + dv->m_channelMarker->getShift();
|
|
nw = -(dsbw / 2) + dv->m_channelMarker->getShift();
|
|
}
|
|
|
|
// draw the DSB rectangle
|
|
|
|
QMatrix4x4 glMatrixDsb;
|
|
glMatrixDsb.setToIdentity();
|
|
glMatrixDsb.translate(
|
|
-1.0f + 2.0f * ((m_leftMargin + m_frequencyScale.getPosFromValue(xc - (dsbw/2))) / (float) width()),
|
|
1.0f
|
|
);
|
|
glMatrixDsb.scale(
|
|
2.0f * (dsbw / (float) frequencySpan),
|
|
-2.0f
|
|
);
|
|
|
|
dv->m_glMatrixDsbWaterfall = glMatrixDsb;
|
|
dv->m_glMatrixDsbWaterfall.translate(
|
|
0.0f,
|
|
(float) m_waterfallTop / (float) height()
|
|
);
|
|
dv->m_glMatrixDsbWaterfall.scale(
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) m_waterfallHeight / (float) height()
|
|
);
|
|
|
|
dv->m_glMatrixDsbHistogram = glMatrixDsb;
|
|
dv->m_glMatrixDsbHistogram.translate(
|
|
0.0f,
|
|
(float) m_histogramTop / (float) height()
|
|
);
|
|
dv->m_glMatrixDsbHistogram.scale(
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) m_histogramHeight / (float) height()
|
|
);
|
|
|
|
dv->m_glMatrixDsbFreqScale = glMatrixDsb;
|
|
dv->m_glMatrixDsbFreqScale.translate(
|
|
0.0f,
|
|
(float) frequencyScaleTop / (float) height()
|
|
);
|
|
dv->m_glMatrixDsbFreqScale.scale(
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) m_frequencyScaleHeight / (float) height()
|
|
);
|
|
|
|
// draw the effective BW rectangle
|
|
|
|
QMatrix4x4 glMatrix;
|
|
glMatrix.setToIdentity();
|
|
glMatrix.translate(
|
|
-1.0f + 2.0f * ((m_leftMargin + m_frequencyScale.getPosFromValue(xc + nw)) / (float) width()),
|
|
1.0f
|
|
);
|
|
glMatrix.scale(
|
|
2.0f * ((pw-nw) / (float) frequencySpan),
|
|
-2.0f
|
|
);
|
|
|
|
dv->m_glMatrixWaterfall = glMatrix;
|
|
dv->m_glMatrixWaterfall.translate(
|
|
0.0f,
|
|
(float) m_waterfallTop / (float) height()
|
|
);
|
|
dv->m_glMatrixWaterfall.scale(
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) m_waterfallHeight / (float) height()
|
|
);
|
|
|
|
dv->m_glMatrixHistogram = glMatrix;
|
|
dv->m_glMatrixHistogram.translate(
|
|
0.0f,
|
|
(float) m_histogramTop / (float) height()
|
|
);
|
|
dv->m_glMatrixHistogram.scale(
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) m_histogramHeight / (float) height()
|
|
);
|
|
|
|
dv->m_glMatrixFreqScale = glMatrix;
|
|
dv->m_glMatrixFreqScale.translate(
|
|
0.0f,
|
|
(float) frequencyScaleTop / (float) height()
|
|
);
|
|
dv->m_glMatrixFreqScale.scale(
|
|
(float) (width() - m_leftMargin - m_rightMargin) / (float) width(),
|
|
(float) m_frequencyScaleHeight / (float) height()
|
|
);
|
|
|
|
|
|
/*
|
|
dv->m_glRect.setRect(
|
|
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)getDisplayedSampleRate()),
|
|
1);
|
|
*/
|
|
|
|
if (m_displayHistogram || m_displayMaxHold || m_displayCurrent || m_displayWaterfall)
|
|
{
|
|
dv->m_rect.setRect(m_frequencyScale.getPosFromValue(xc) + m_leftMargin - 1,
|
|
m_topMargin,
|
|
5,
|
|
height() - m_topMargin - m_bottomMargin);
|
|
}
|
|
|
|
/*
|
|
if(m_displayHistogram || m_displayMaxHold || m_displayWaterfall) {
|
|
dv->m_rect.setRect(m_frequencyScale.getPosFromValue(centerFrequency + dv->m_channelMarker->getCenterFrequency()) + m_leftMargin - 1,
|
|
m_topMargin,
|
|
5,
|
|
height() - m_topMargin - m_bottomMargin);
|
|
}
|
|
*/
|
|
}
|
|
|
|
// prepare left scales (time and power)
|
|
paintLeftScales();
|
|
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);
|
|
m_frequencyPixmap.fill(Qt::transparent);
|
|
{
|
|
QPainter painter(&m_frequencyPixmap);
|
|
painter.setPen(Qt::NoPen);
|
|
painter.setBrush(Qt::black);
|
|
painter.setBrush(Qt::transparent);
|
|
painter.drawRect(m_leftMargin, 0, width() - m_leftMargin, m_frequencyScaleHeight);
|
|
painter.setPen(QColor(0xf0, 0xf0, 0xff));
|
|
painter.setFont(font());
|
|
const ScaleEngine::TickList* tickList = &m_frequencyScale.getTickList();
|
|
const ScaleEngine::Tick* tick;
|
|
|
|
for (int i = 0; i < tickList->count(); i++) {
|
|
tick = &(*tickList)[i];
|
|
if (tick->major) {
|
|
if (tick->textSize > 0)
|
|
painter.drawText(QPointF(m_leftMargin + tick->textPos, fm.height() + fm.ascent() / 2 - 1), tick->text);
|
|
}
|
|
}
|
|
|
|
// Frequency overlay on highlighted marker
|
|
for (int i = 0; i < m_channelMarkerStates.size(); ++i)
|
|
{
|
|
ChannelMarkerState* dv = m_channelMarkerStates[i];
|
|
|
|
if (dv->m_channelMarker->getHighlighted()
|
|
&& (dv->m_channelMarker->getSourceOrSinkStream() == m_displaySourceOrSink)
|
|
&& dv->m_channelMarker->streamIndexApplies(m_displayStreamIndex))
|
|
{
|
|
qreal xc;
|
|
int shift;
|
|
//ChannelMarker::sidebands_t sidebands = dv->m_channelMarker->getSidebands();
|
|
xc = centerFrequency + dv->m_channelMarker->getCenterFrequency(); // marker center frequency
|
|
QString ftext;
|
|
switch (dv->m_channelMarker->getFrequencyScaleDisplayType())
|
|
{
|
|
case ChannelMarker::FScaleDisplay_freq:
|
|
ftext = QString::number((centerFrequency + dv->m_channelMarker->getCenterFrequency())/1e6, 'f', 6);
|
|
break;
|
|
case ChannelMarker::FScaleDisplay_title:
|
|
ftext = dv->m_channelMarker->getTitle();
|
|
break;
|
|
case ChannelMarker::FScaleDisplay_addressSend:
|
|
ftext = dv->m_channelMarker->getDisplayAddressSend();
|
|
break;
|
|
case ChannelMarker::FScaleDisplay_addressReceive:
|
|
ftext = dv->m_channelMarker->getDisplayAddressReceive();
|
|
break;
|
|
default:
|
|
ftext = QString::number((centerFrequency + dv->m_channelMarker->getCenterFrequency())/1e6, 'f', 6);
|
|
break;
|
|
}
|
|
if (dv->m_channelMarker->getCenterFrequency() < 0) { // left half of scale
|
|
ftext = " " + ftext;
|
|
shift = 0;
|
|
} else { // right half of scale
|
|
ftext = ftext + " ";
|
|
shift = - fm.horizontalAdvance(ftext);
|
|
}
|
|
painter.drawText(QPointF(m_leftMargin + m_frequencyScale.getPosFromValue(xc) + shift, 2*fm.height() + fm.ascent() / 2 - 1), ftext);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
m_glShaderFrequencyScale.initTexture(m_frequencyPixmap.toImage());
|
|
}
|
|
// prepare left scale for spectrogram (time)
|
|
{
|
|
m_spectrogramTimePixmap = QPixmap(m_leftMargin - 1, fm.ascent() + m_waterfallHeight);
|
|
m_spectrogramTimePixmap.fill(Qt::transparent);
|
|
{
|
|
QPainter painter(&m_spectrogramTimePixmap);
|
|
painter.setPen(QColor(0xf0, 0xf0, 0xff));
|
|
painter.setFont(font());
|
|
const ScaleEngine::TickList* tickList;
|
|
const ScaleEngine::Tick* tick;
|
|
if (m_display3DSpectrogram) {
|
|
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, fm.height() + tick->textPos), tick->text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_glShaderSpectrogramTimeScale.initTexture(m_spectrogramTimePixmap.toImage());
|
|
}
|
|
// prepare vertical scale for spectrogram (power)
|
|
{
|
|
int h = m_histogramHeight != 0 ? m_histogramHeight : m_waterfallHeight / 4;
|
|
m_spectrogramPowerPixmap = QPixmap(m_leftMargin - 1, m_topMargin + h);
|
|
m_spectrogramPowerPixmap.fill(Qt::transparent);
|
|
{
|
|
QPainter painter(&m_spectrogramPowerPixmap);
|
|
painter.setPen(QColor(0xf0, 0xf0, 0xff));
|
|
painter.setFont(font());
|
|
const ScaleEngine::TickList* tickList;
|
|
const ScaleEngine::Tick* tick;
|
|
if (m_display3DSpectrogram) {
|
|
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_topMargin + h - tick->textPos - 1), tick->text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_glShaderSpectrogramPowerScale.initTexture(m_spectrogramPowerPixmap.toImage());
|
|
}
|
|
|
|
// Top info line
|
|
m_glInfoBoxMatrix.setToIdentity();
|
|
m_glInfoBoxMatrix.translate (
|
|
-1.0f,
|
|
1.0f
|
|
);
|
|
m_glInfoBoxMatrix.scale (
|
|
2.0f,
|
|
(float) -2*m_infoHeight / (float) height()
|
|
);
|
|
m_infoRect = QRect(
|
|
0,
|
|
0,
|
|
width(),
|
|
m_infoHeight
|
|
);
|
|
QString infoText;
|
|
formatTextInfo(infoText);
|
|
m_infoPixmap = QPixmap(width(), m_infoHeight);
|
|
m_infoPixmap.fill(Qt::transparent);
|
|
{
|
|
QPainter painter(&m_infoPixmap);
|
|
painter.setPen(Qt::NoPen);
|
|
painter.setBrush(Qt::black);
|
|
painter.setBrush(Qt::transparent);
|
|
painter.drawRect(m_leftMargin, 0, width() - m_leftMargin, m_infoHeight);
|
|
painter.setPen(QColor(0xf0, 0xf0, 0xff));
|
|
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(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) {
|
|
waterfallFFTSizeChanged = m_waterfallBuffer->width() != m_nbBins + 1;
|
|
}
|
|
|
|
bool windowSizeChanged = m_waterfallTextureHeight != m_waterfallHeight;
|
|
|
|
if (waterfallFFTSizeChanged || windowSizeChanged)
|
|
{
|
|
if (m_waterfallBuffer) {
|
|
delete m_waterfallBuffer;
|
|
}
|
|
|
|
m_waterfallBuffer = new QImage(m_nbBins + 1, m_waterfallHeight, QImage::Format_ARGB32);
|
|
m_waterfallBuffer->fill(qRgb(0x00, 0x00, 0x00));
|
|
|
|
if (m_waterfallHeight > 0) {
|
|
m_glShaderWaterfall.initTexture(*m_waterfallBuffer);
|
|
}
|
|
|
|
m_waterfallBufferPos = 0;
|
|
|
|
if (m_3DSpectrogramBuffer) {
|
|
delete m_3DSpectrogramBuffer;
|
|
}
|
|
|
|
m_3DSpectrogramBuffer = new QImage(m_nbBins + 1, m_waterfallHeight, QImage::Format_Grayscale8);
|
|
m_3DSpectrogramBuffer->fill(qRgb(0x00, 0x00, 0x00));
|
|
|
|
if (m_waterfallHeight > 0) {
|
|
m_glShaderSpectrogram.initTexture(*m_3DSpectrogramBuffer);
|
|
}
|
|
|
|
m_3DSpectrogramBufferPos = 0;
|
|
|
|
m_waterfallTextureHeight = m_waterfallHeight;
|
|
m_waterfallTexturePos = 0;
|
|
m_3DSpectrogramTextureHeight = m_waterfallHeight;
|
|
m_3DSpectrogramTexturePos = 0;
|
|
}
|
|
|
|
bool histogramFFTSizeChanged = true;
|
|
|
|
if (m_histogramBuffer) {
|
|
histogramFFTSizeChanged = m_histogramBuffer->width() != m_nbBins;
|
|
}
|
|
|
|
if (histogramFFTSizeChanged)
|
|
{
|
|
if (m_histogramBuffer) {
|
|
delete m_histogramBuffer;
|
|
}
|
|
|
|
m_histogramBuffer = new QImage(m_nbBins, 100, QImage::Format_RGB32);
|
|
|
|
m_histogramBuffer->fill(qRgb(0x00, 0x00, 0x00));
|
|
m_glShaderHistogram.initTexture(*m_histogramBuffer, QOpenGLTexture::ClampToEdge);
|
|
|
|
if (m_histogram) {
|
|
delete[] m_histogram;
|
|
}
|
|
|
|
m_histogram = new quint8[100 * m_nbBins];
|
|
memset(m_histogram, 0x00, 100 * m_nbBins);
|
|
|
|
m_q3FFT.allocate(2*(m_nbBins+1));
|
|
|
|
m_q3ColorMap.allocate(4*(m_nbBins+1));
|
|
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
|
|
updateHistogramMarkers();
|
|
updateWaterfallMarkers();
|
|
updateSortedAnnotationMarkers();
|
|
} // applyChanges
|
|
|
|
void GLSpectrumView::updateHistogramMarkers()
|
|
{
|
|
if (getDisplayedSampleRate() == 0) {
|
|
return;
|
|
}
|
|
int64_t centerFrequency;
|
|
int frequencySpan;
|
|
getFrequencyZoom(centerFrequency, frequencySpan);
|
|
int effFftSize = m_fftSize * ((float) frequencySpan / (float) getDisplayedSampleRate());
|
|
|
|
for (int i = 0; i < m_histogramMarkers.size(); i++)
|
|
{
|
|
float powerI = m_linear ?
|
|
m_histogramMarkers.at(i).m_power * (m_useCalibration ? m_calibrationGain : 1.0f) :
|
|
CalcDb::dbPower(m_histogramMarkers.at(i).m_power) + (m_useCalibration ? m_calibrationShiftdB : 0.0f);
|
|
m_histogramMarkers[i].m_point.rx() =
|
|
(m_histogramMarkers[i].m_frequency - m_frequencyScale.getRangeMin()) / m_frequencyScale.getRange();
|
|
m_histogramMarkers[i].m_point.ry() =
|
|
(m_powerScale.getRangeMax() - powerI) / m_powerScale.getRange();
|
|
// m_histogramMarkers[i].m_fftBin =
|
|
// (((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 ?
|
|
0 : m_histogramMarkers[i].m_point.rx() > 1 ?
|
|
1 : m_histogramMarkers[i].m_point.rx();
|
|
m_histogramMarkers[i].m_point.ry() = m_histogramMarkers[i].m_point.ry() < 0 ?
|
|
0 : m_histogramMarkers[i].m_point.ry() > 1 ?
|
|
1 : m_histogramMarkers[i].m_point.ry();
|
|
m_histogramMarkers[i].m_fftBin = m_histogramMarkers[i].m_fftBin < 0 ?
|
|
0 : m_histogramMarkers[i].m_fftBin > m_fftSize - 1 ?
|
|
m_fftSize - 1 : m_histogramMarkers[i].m_fftBin;
|
|
m_histogramMarkers[i].m_frequencyStr = displayScaled(
|
|
m_histogramMarkers[i].m_frequency,
|
|
'f',
|
|
getPrecision((centerFrequency*1000)/getDisplayedSampleRate()),
|
|
false);
|
|
m_histogramMarkers[i].m_powerStr = displayPower(
|
|
powerI,
|
|
m_linear ? 'e' : 'f',
|
|
m_linear ? 3 : 1);
|
|
|
|
if (i > 0)
|
|
{
|
|
int64_t deltaFrequency = m_histogramMarkers.at(i).m_frequency - m_histogramMarkers.at(0).m_frequency;
|
|
m_histogramMarkers[i].m_deltaFrequencyStr = displayScaled(
|
|
deltaFrequency,
|
|
'f',
|
|
getPrecision(deltaFrequency/getDisplayedSampleRate()),
|
|
true);
|
|
float power0 = m_linear ?
|
|
m_histogramMarkers.at(0).m_power * (m_useCalibration ? m_calibrationGain : 1.0f) :
|
|
CalcDb::dbPower(m_histogramMarkers.at(0).m_power) + (m_useCalibration ? m_calibrationShiftdB : 0.0f);
|
|
m_histogramMarkers[i].m_deltaPowerStr = displayPower(
|
|
powerI - power0,
|
|
m_linear ? 'e' : 'f',
|
|
m_linear ? 3 : 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::updateHistogramPeaks()
|
|
{
|
|
int j = 0;
|
|
for (int i = 0; i < m_histogramMarkers.size(); i++)
|
|
{
|
|
if (j >= (int) m_peakFinder.getPeaks().size()) {
|
|
break;
|
|
}
|
|
|
|
int fftBin = m_peakFinder.getPeaks()[j].second;
|
|
Real power = m_peakFinder.getPeaks()[j].first;
|
|
// qDebug("GLSpectrumView::updateHistogramPeaks: %d %d %f", j, fftBin, power);
|
|
|
|
if ((m_histogramMarkers.at(i).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePower) ||
|
|
((m_histogramMarkers.at(i).m_markerType == SpectrumHistogramMarker::SpectrumMarkerTypePowerMax) &&
|
|
(m_histogramMarkers.at(i).m_holdReset || (power > m_histogramMarkers.at(i).m_powerMax))))
|
|
{
|
|
float binSize = m_frequencyScale.getRange() / m_nbBins;
|
|
m_histogramMarkers[i].m_fftBin = fftBin;
|
|
m_histogramMarkers[i].m_frequency = m_frequencyScale.getRangeMin() + binSize*fftBin;
|
|
m_histogramMarkers[i].m_point.rx() = binSize*fftBin / m_frequencyScale.getRange();
|
|
|
|
if (i == 0)
|
|
{
|
|
m_histogramMarkers[i].m_frequencyStr = displayScaled(
|
|
m_histogramMarkers[i].m_frequency,
|
|
'f',
|
|
getPrecision((getDisplayedCenterFrequency()*1000)/getDisplayedSampleRate()),
|
|
false
|
|
);
|
|
}
|
|
else
|
|
{
|
|
int64_t deltaFrequency = m_histogramMarkers.at(i).m_frequency - m_histogramMarkers.at(0).m_frequency;
|
|
m_histogramMarkers[i].m_deltaFrequencyStr = displayScaled(
|
|
deltaFrequency,
|
|
'f',
|
|
getPrecision(deltaFrequency/getDisplayedSampleRate()),
|
|
true
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
|
|
j++;
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::updateWaterfallMarkers()
|
|
{
|
|
for (int i = 0; i < m_waterfallMarkers.size(); i++)
|
|
{
|
|
m_waterfallMarkers[i].m_point.rx() =
|
|
(m_waterfallMarkers[i].m_frequency - m_frequencyScale.getRangeMin()) / m_frequencyScale.getRange();
|
|
m_waterfallMarkers[i].m_point.ry() =
|
|
(m_waterfallMarkers[i].m_time - m_timeScale.getRangeMin()) / m_timeScale.getRange();
|
|
m_waterfallMarkers[i].m_point.rx() = m_waterfallMarkers[i].m_point.rx() < 0 ?
|
|
0 : m_waterfallMarkers[i].m_point.rx() > 1 ?
|
|
1 : m_waterfallMarkers[i].m_point.rx();
|
|
m_waterfallMarkers[i].m_point.ry() = m_waterfallMarkers[i].m_point.ry() < 0 ?
|
|
0 : m_waterfallMarkers[i].m_point.ry() > 1 ?
|
|
1 : m_waterfallMarkers[i].m_point.ry();
|
|
m_waterfallMarkers[i].m_frequencyStr = displayScaled(
|
|
m_waterfallMarkers[i].m_frequency,
|
|
'f',
|
|
getPrecision((getDisplayedCenterFrequency()*1000)/getDisplayedSampleRate()),
|
|
false);
|
|
m_waterfallMarkers[i].m_timeStr = displayScaledF(
|
|
m_waterfallMarkers[i].m_time,
|
|
'f',
|
|
3,
|
|
true);
|
|
|
|
if (i > 0)
|
|
{
|
|
int64_t deltaFrequency = m_waterfallMarkers.at(i).m_frequency - m_waterfallMarkers.at(0).m_frequency;
|
|
m_waterfallMarkers.back().m_deltaFrequencyStr = displayScaled(
|
|
deltaFrequency,
|
|
'f',
|
|
getPrecision(deltaFrequency/getDisplayedSampleRate()),
|
|
true);
|
|
m_waterfallMarkers.back().m_deltaTimeStr = displayScaledF(
|
|
m_waterfallMarkers.at(i).m_time - m_waterfallMarkers.at(0).m_time,
|
|
'f',
|
|
3,
|
|
true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::updateAnnotationMarkers()
|
|
{
|
|
emit updateAnnotations(); // Notify other plugins we have updated annotations
|
|
|
|
if (!(m_markersDisplay & SpectrumSettings::MarkersDisplayAnnotations)) {
|
|
return;
|
|
}
|
|
|
|
m_sortedAnnotationMarkers.clear();
|
|
|
|
for (auto &marker : m_annotationMarkers) {
|
|
m_sortedAnnotationMarkers.push_back(&marker);
|
|
}
|
|
|
|
std::sort(m_sortedAnnotationMarkers.begin(), m_sortedAnnotationMarkers.end(), annotationDisplayLessThan);
|
|
updateSortedAnnotationMarkers();
|
|
}
|
|
|
|
void GLSpectrumView::updateSortedAnnotationMarkers()
|
|
{
|
|
if (!(m_markersDisplay & SpectrumSettings::MarkersDisplayAnnotations)) {
|
|
return;
|
|
}
|
|
|
|
m_visibleAnnotationMarkers.clear();
|
|
|
|
for (auto &marker : m_sortedAnnotationMarkers)
|
|
{
|
|
float startPos = (marker->m_startFrequency - m_frequencyScale.getRangeMin()) / m_frequencyScale.getRange();
|
|
float stopPos = ((marker->m_startFrequency + marker->m_bandwidth) - m_frequencyScale.getRangeMin()) / m_frequencyScale.getRange();
|
|
|
|
if ((startPos > 1.0f) || (stopPos < 0.0f)) // out of range
|
|
{
|
|
continue;
|
|
}
|
|
|
|
m_visibleAnnotationMarkers.push_back(marker);
|
|
m_visibleAnnotationMarkers.back()->m_startPos = startPos < 0.0f ? 0.0f : startPos;
|
|
m_visibleAnnotationMarkers.back()->m_stopPos = stopPos > 1.0f ? 1.0f : stopPos;
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::updateMarkersDisplay()
|
|
{
|
|
if (m_markersDisplay & SpectrumSettings::MarkersDisplayAnnotations) {
|
|
updateAnnotationMarkers();
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::updateCalibrationPoints()
|
|
{
|
|
if (m_calibrationPoints.size() == 0)
|
|
{
|
|
m_calibrationGain = 1.0;
|
|
m_calibrationShiftdB = 0.0;
|
|
}
|
|
else if (m_calibrationPoints.size() == 1)
|
|
{
|
|
m_calibrationGain = m_calibrationPoints.first().m_powerCalibratedReference /
|
|
m_calibrationPoints.first().m_powerRelativeReference;
|
|
m_calibrationShiftdB = CalcDb::dbPower(m_calibrationGain);
|
|
}
|
|
else
|
|
{
|
|
QList<SpectrumCalibrationPoint> sortedCalibrationPoints = m_calibrationPoints;
|
|
std::sort(sortedCalibrationPoints.begin(), sortedCalibrationPoints.end(), calibrationPointsLessThan);
|
|
|
|
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 (getDisplayedCenterFrequency() >= sortedCalibrationPoints.last().m_frequency)
|
|
{
|
|
m_calibrationGain = m_calibrationPoints.last().m_powerCalibratedReference /
|
|
m_calibrationPoints.last().m_powerRelativeReference;
|
|
m_calibrationShiftdB = CalcDb::dbPower(m_calibrationGain);
|
|
}
|
|
else
|
|
{
|
|
int lowIndex = 0;
|
|
int highIndex = sortedCalibrationPoints.size() - 1;
|
|
|
|
for (int index = 0; index < sortedCalibrationPoints.size(); index++)
|
|
{
|
|
if (getDisplayedCenterFrequency() < sortedCalibrationPoints[index].m_frequency)
|
|
{
|
|
highIndex = index;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
lowIndex = index;
|
|
}
|
|
}
|
|
|
|
// frequency interpolation is always linear
|
|
double deltaFrequency = sortedCalibrationPoints[highIndex].m_frequency -
|
|
sortedCalibrationPoints[lowIndex].m_frequency;
|
|
double shiftFrequency = getDisplayedCenterFrequency() - sortedCalibrationPoints[lowIndex].m_frequency;
|
|
double interpolationRatio = shiftFrequency / deltaFrequency;
|
|
|
|
// calculate low and high gains in linear mode
|
|
double gainLow = sortedCalibrationPoints[lowIndex].m_powerCalibratedReference /
|
|
sortedCalibrationPoints[lowIndex].m_powerRelativeReference;
|
|
double gainHigh = sortedCalibrationPoints[highIndex].m_powerCalibratedReference /
|
|
sortedCalibrationPoints[highIndex].m_powerRelativeReference;
|
|
|
|
// power interpolation depends on interpolation options
|
|
if (m_calibrationInterpMode == SpectrumSettings::CalibInterpLinear)
|
|
{
|
|
m_calibrationGain = gainLow + interpolationRatio*(gainHigh - gainLow); // linear driven
|
|
m_calibrationShiftdB = CalcDb::dbPower(m_calibrationGain);
|
|
}
|
|
else if (m_calibrationInterpMode == SpectrumSettings::CalibInterpLog)
|
|
{
|
|
m_calibrationShiftdB = CalcDb::dbPower(gainLow)
|
|
+ interpolationRatio*(CalcDb::dbPower(gainHigh) - CalcDb::dbPower(gainLow)); // log driven
|
|
m_calibrationGain = CalcDb::powerFromdB(m_calibrationShiftdB);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateHistogramMarkers();
|
|
|
|
if (m_messageQueueToGUI && m_useCalibration) {
|
|
m_messageQueueToGUI->push(new MsgReportCalibrationShift(m_calibrationShiftdB));
|
|
}
|
|
|
|
m_changesPending = true;
|
|
}
|
|
|
|
bool GLSpectrumView::event(QEvent* event)
|
|
{
|
|
if (event->type() == QEvent::Gesture)
|
|
{
|
|
QGestureEvent *gestureEvent = static_cast<QGestureEvent *>(event);
|
|
|
|
if (QPanGesture *pan = static_cast<QPanGesture *>(gestureEvent->gesture(Qt::PanGesture)))
|
|
{
|
|
if (pan->state() == Qt::GestureStarted)
|
|
{
|
|
m_scrollStartCenterFreq = getDisplayedCenterFrequency();
|
|
}
|
|
else if (pan->state() == Qt::GestureUpdated)
|
|
{
|
|
QPointF offset = pan->offset();
|
|
float histogramWidth = width() - m_leftMargin - m_rightMargin;
|
|
qint64 frequency = (qint64)(m_scrollStartCenterFreq + -offset.x()/histogramWidth * m_frequencyScale.getRange());
|
|
queueRequestCenterFrequency(frequency);
|
|
}
|
|
return true;
|
|
}
|
|
else if (QPinchGesture *pinch = static_cast<QPinchGesture *>(gestureEvent->gesture(Qt::PinchGesture)))
|
|
{
|
|
// Don't get GestureStarted and startCenterPoint is always 0,0
|
|
// https://bugreports.qt.io/browse/QTBUG-109205
|
|
if (!m_pinching)
|
|
{
|
|
m_scrollStartCenterFreq = getDisplayedCenterFrequency();
|
|
m_pinchStart = pinch->centerPoint();
|
|
m_pinching = true;
|
|
m_pinching3D = m_display3DSpectrogram && pointInWaterfallOrSpectrogram(mapFromGlobal(m_pinchStart.toPoint()));
|
|
}
|
|
else
|
|
{
|
|
if (pinch->changeFlags() & QPinchGesture::CenterPointChanged)
|
|
{
|
|
if (!m_pinching3D)
|
|
{
|
|
// Scroll frequency up or down
|
|
QPointF offset = pinch->centerPoint() - m_pinchStart;
|
|
float histogramWidth = width() - m_leftMargin - m_rightMargin;
|
|
qint64 frequency = (qint64)(m_scrollStartCenterFreq + -offset.x()/histogramWidth * m_frequencyScale.getRange());
|
|
queueRequestCenterFrequency(frequency);
|
|
}
|
|
}
|
|
if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged)
|
|
{
|
|
if (!m_pinching3D)
|
|
{
|
|
// Zoom in/out of spectrum
|
|
QPoint p = mapFromGlobal(pinch->centerPoint().toPoint());
|
|
zoomFactor(p, pinch->scaleFactor());
|
|
}
|
|
else
|
|
{
|
|
// Scale Z axis of 3D spectragram
|
|
m_glShaderSpectrogram.userScaleZ(pinch->scaleFactor());
|
|
}
|
|
}
|
|
if (pinch->state() == Qt::GestureFinished)
|
|
{
|
|
m_pinching = false;
|
|
m_pinching3D = false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return QOpenGLWidget::event(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
|
|
QPointF delta = m_mousePrevLocalPos - event->localPos();
|
|
m_mousePrevLocalPos = event->localPos();
|
|
m_glShaderSpectrogram.rotateZ(-delta.x()/2.0f);
|
|
m_glShaderSpectrogram.rotateX(-delta.y()/2.0f);
|
|
repaint(); // Force repaint in case acquisition is stopped
|
|
return;
|
|
}
|
|
if (m_pan3DSpectrogram)
|
|
{
|
|
// Pan 3D Spectrogram
|
|
QPointF delta = m_mousePrevLocalPos - event->localPos();
|
|
m_mousePrevLocalPos = event->localPos();
|
|
m_glShaderSpectrogram.translateX(-delta.x()/2.0f/500.0f);
|
|
m_glShaderSpectrogram.translateY(delta.y()/2.0f/500.0f);
|
|
repaint(); // Force repaint in case acquisition is stopped
|
|
return;
|
|
}
|
|
|
|
if (m_scaleZ3DSpectrogram)
|
|
{
|
|
// Scale 3D Spectrogram in Z dimension
|
|
QPointF delta = m_mousePrevLocalPos - event->localPos();
|
|
m_mousePrevLocalPos = event->localPos();
|
|
m_glShaderSpectrogram.userScaleZ(1.0+(float)delta.y()/20.0);
|
|
repaint(); // Force repaint in case acquisition is stopped
|
|
return;
|
|
}
|
|
|
|
if (m_scrollFrequency)
|
|
{
|
|
// Request containing widget to adjust center frequency
|
|
// Not all containers will support this - mainly for MainSpectrumGUI
|
|
// This can be a little slow on some SDRs, so we use delta from where
|
|
// button was originally pressed rather than do it incrementally
|
|
QPointF delta = m_mousePrevLocalPos - event->localPos();
|
|
float histogramWidth = width() - m_leftMargin - m_rightMargin;
|
|
qint64 frequency = (qint64)(m_scrollStartCenterFreq + delta.x()/histogramWidth * m_frequencyScale.getRange());
|
|
queueRequestCenterFrequency(frequency);
|
|
return;
|
|
}
|
|
|
|
if (m_displayWaterfall || m_displayHistogram || m_displayMaxHold || m_displayCurrent)
|
|
{
|
|
if (m_frequencyScaleRect.contains(event->pos()))
|
|
{
|
|
if (m_cursorState == CSNormal)
|
|
{
|
|
setCursor(Qt::SizeVerCursor);
|
|
m_cursorState = CSSplitter;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (m_cursorState == CSSplitter)
|
|
{
|
|
setCursor(Qt::ArrowCursor);
|
|
m_cursorState = CSNormal;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_cursorState == CSSplitterMoving)
|
|
{
|
|
QMutexLocker mutexLocker(&m_mutex);
|
|
float newShare;
|
|
|
|
if (!m_invertedWaterfall) {
|
|
newShare = (float) (event->y() - m_frequencyScaleRect.height()) / (float) height();
|
|
} else {
|
|
newShare = 1.0 - (float) (event->y() + m_frequencyScaleRect.height()) / (float) height();
|
|
}
|
|
|
|
if (newShare < 0.1) {
|
|
newShare = 0.1f;
|
|
} else if (newShare > 0.8) {
|
|
newShare = 0.8f;
|
|
}
|
|
|
|
m_waterfallShare = newShare;
|
|
m_changesPending = true;
|
|
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportWaterfallShare(m_waterfallShare));
|
|
}
|
|
|
|
update();
|
|
return;
|
|
}
|
|
else if (m_cursorState == CSChannelMoving)
|
|
{
|
|
// Determine if user is trying to move the channel outside of the current frequency range
|
|
// 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 = getDisplayedCenterFrequency() - getDisplayedSampleRate() / 2.0f;
|
|
Real freqMax = getDisplayedCenterFrequency() + getDisplayedSampleRate() / 2.0f;
|
|
if (freqAbs < freqMin) {
|
|
queueRequestCenterFrequency(getDisplayedCenterFrequency() - (freqMin - freqAbs));
|
|
} else if (freqAbs > freqMax) {
|
|
queueRequestCenterFrequency(getDisplayedCenterFrequency() + (freqAbs - freqMax));
|
|
}
|
|
|
|
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))
|
|
{
|
|
m_channelMarkerStates[m_cursorChannel]->m_channelMarker->setCenterFrequencyByCursor(freq);
|
|
channelMarkerChanged();
|
|
}
|
|
}
|
|
|
|
if (m_displayWaterfall || m_displayHistogram || m_displayMaxHold || m_displayCurrent)
|
|
{
|
|
for (int i = 0; i < m_channelMarkerStates.size(); ++i)
|
|
{
|
|
if ((m_channelMarkerStates[i]->m_channelMarker->getSourceOrSinkStream() != m_displaySourceOrSink)
|
|
|| !m_channelMarkerStates[i]->m_channelMarker->streamIndexApplies(m_displayStreamIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (m_channelMarkerStates[i]->m_rect.contains(event->pos()))
|
|
{
|
|
if (m_cursorState == CSNormal)
|
|
{
|
|
setCursor(Qt::SizeHorCursor);
|
|
m_cursorState = CSChannel;
|
|
m_cursorChannel = i;
|
|
m_channelMarkerStates[i]->m_channelMarker->setHighlightedByCursor(true);
|
|
channelMarkerChanged();
|
|
|
|
return;
|
|
}
|
|
else if (m_cursorState == CSChannel)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else if (m_channelMarkerStates[i]->m_channelMarker->getHighlighted())
|
|
{
|
|
// Don't clear highlight while dragging a channel, as we want the
|
|
// frequency of the channel to be continuously displayed
|
|
if (m_cursorState != CSChannelMoving)
|
|
{
|
|
m_channelMarkerStates[i]->m_channelMarker->setHighlightedByCursor(false);
|
|
channelMarkerChanged();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_cursorState == CSChannel)
|
|
{
|
|
setCursor(Qt::ArrowCursor);
|
|
m_cursorState = CSNormal;
|
|
|
|
return;
|
|
}
|
|
|
|
event->setAccepted(false);
|
|
}
|
|
|
|
void GLSpectrumView::mousePressEvent(QMouseEvent* event)
|
|
{
|
|
const QPointF& ep = event->localPos();
|
|
|
|
if ((event->button() == Qt::MiddleButton) && (m_displayMaxHold || m_displayCurrent || m_displayHistogram) && pointInHistogram(ep))
|
|
{
|
|
m_scrollFrequency = true;
|
|
m_scrollStartCenterFreq = getDisplayedCenterFrequency();
|
|
m_mousePrevLocalPos = ep;
|
|
return;
|
|
}
|
|
|
|
if ((event->button() == Qt::MiddleButton) && m_display3DSpectrogram && pointInWaterfallOrSpectrogram(ep))
|
|
{
|
|
m_pan3DSpectrogram = true;
|
|
m_mousePrevLocalPos = ep;
|
|
return;
|
|
}
|
|
|
|
if ((event->button() == Qt::RightButton) && m_display3DSpectrogram && pointInWaterfallOrSpectrogram(ep))
|
|
{
|
|
m_scaleZ3DSpectrogram = true;
|
|
m_mousePrevLocalPos = ep;
|
|
return;
|
|
}
|
|
|
|
if (event->button() == Qt::RightButton)
|
|
{
|
|
QPointF pHis = ep;
|
|
bool doUpdate = false;
|
|
pHis.rx() = (ep.x()/width() - m_histogramRect.left()) / m_histogramRect.width();
|
|
pHis.ry() = (ep.y()/height() - m_histogramRect.top()) / m_histogramRect.height();
|
|
|
|
if (event->modifiers() & Qt::ShiftModifier)
|
|
{
|
|
if ((pHis.x() >= 0) && (pHis.x() <= 1) && (pHis.y() >= 0) && (pHis.y() <= 1))
|
|
{
|
|
m_histogramMarkers.clear();
|
|
doUpdate = true;
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportHistogramMarkersChange());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ((m_histogramMarkers.size() > 0) && (pHis.x() >= 0) && (pHis.x() <= 1) && (pHis.y() >= 0) && (pHis.y() <= 1))
|
|
{
|
|
m_histogramMarkers.pop_back();
|
|
doUpdate = true;
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportHistogramMarkersChange());
|
|
}
|
|
}
|
|
}
|
|
|
|
QPointF pWat = ep;
|
|
pWat.rx() = (ep.x()/width() - m_waterfallRect.left()) / m_waterfallRect.width();
|
|
pWat.ry() = (ep.y()/height() - m_waterfallRect.top()) / m_waterfallRect.height();
|
|
|
|
if (event->modifiers() & Qt::ShiftModifier)
|
|
{
|
|
if ((pWat.x() >= 0) && (pWat.x() <= 1) && (pWat.y() >= 0) && (pWat.y() <= 1))
|
|
{
|
|
m_waterfallMarkers.clear();
|
|
doUpdate = true;
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportWaterfallMarkersChange());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ((m_waterfallMarkers.size() > 0) && (pWat.x() >= 0) && (pWat.x() <= 1) && (pWat.y() >= 0) && (pWat.y() <= 1))
|
|
{
|
|
m_waterfallMarkers.pop_back();
|
|
doUpdate = true;
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportWaterfallMarkersChange());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (doUpdate) {
|
|
update();
|
|
}
|
|
}
|
|
else if (event->button() == Qt::LeftButton)
|
|
{
|
|
if (event->modifiers() & Qt::ShiftModifier)
|
|
{
|
|
QPointF pHis = ep;
|
|
bool doUpdate = false;
|
|
pHis.rx() = (ep.x()/width() - m_histogramRect.left()) / m_histogramRect.width();
|
|
pHis.ry() = (ep.y()/height() - m_histogramRect.top()) / m_histogramRect.height();
|
|
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 - getDisplayedCenterFrequency()) / (float) getDisplayedSampleRate()) * m_fftSize) + (m_fftSize / 2);
|
|
|
|
if ((pHis.x() >= 0) && (pHis.x() <= 1) && (pHis.y() >= 0) && (pHis.y() <= 1))
|
|
{
|
|
if (m_histogramMarkers.size() < SpectrumHistogramMarker::m_maxNbOfMarkers)
|
|
{
|
|
m_histogramMarkers.push_back(SpectrumHistogramMarker());
|
|
m_histogramMarkers.back().m_point = pHis;
|
|
m_histogramMarkers.back().m_frequency = frequency;
|
|
m_histogramMarkers.back().m_fftBin = fftBin;
|
|
m_histogramMarkers.back().m_frequencyStr = displayScaled(
|
|
frequency,
|
|
'f',
|
|
getPrecision((getDisplayedCenterFrequency()*1000)/getDisplayedSampleRate()),
|
|
false);
|
|
m_histogramMarkers.back().m_power = power;
|
|
m_histogramMarkers.back().m_powerStr = displayPower(
|
|
powerVal,
|
|
m_linear ? 'e' : 'f',
|
|
m_linear ? 3 : 1);
|
|
|
|
if (m_histogramMarkers.size() > 1)
|
|
{
|
|
int64_t deltaFrequency = frequency - m_histogramMarkers.at(0).m_frequency;
|
|
m_histogramMarkers.back().m_deltaFrequencyStr = displayScaled(
|
|
deltaFrequency,
|
|
'f',
|
|
getPrecision(deltaFrequency/getDisplayedSampleRate()),
|
|
true);
|
|
float power0 = m_linear ?
|
|
m_histogramMarkers.at(0).m_power :
|
|
CalcDb::dbPower(m_histogramMarkers.at(0).m_power);
|
|
m_histogramMarkers.back().m_deltaPowerStr = displayPower(
|
|
power - power0,
|
|
m_linear ? 'e' : 'f',
|
|
m_linear ? 3 : 1);
|
|
}
|
|
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportHistogramMarkersChange());
|
|
}
|
|
doUpdate = true;
|
|
}
|
|
}
|
|
|
|
QPointF pWat = ep;
|
|
pWat.rx() = (ep.x()/width() - m_waterfallRect.left()) / m_waterfallRect.width();
|
|
pWat.ry() = (ep.y()/height() - m_waterfallRect.top()) / m_waterfallRect.height();
|
|
frequency = m_frequencyScale.getRangeMin() + pWat.x()*m_frequencyScale.getRange();
|
|
float time = m_timeScale.getRangeMin() + pWat.y()*m_timeScale.getRange();
|
|
|
|
if ((pWat.x() >= 0) && (pWat.x() <= 1) && (pWat.y() >= 0) && (pWat.y() <= 1) && !m_display3DSpectrogram)
|
|
{
|
|
if (m_waterfallMarkers.size() < SpectrumWaterfallMarker::m_maxNbOfMarkers)
|
|
{
|
|
m_waterfallMarkers.push_back(SpectrumWaterfallMarker());
|
|
m_waterfallMarkers.back().m_point = pWat;
|
|
m_waterfallMarkers.back().m_frequency = frequency;
|
|
m_waterfallMarkers.back().m_frequencyStr = displayScaled(
|
|
frequency,
|
|
'f',
|
|
getPrecision((getDisplayedCenterFrequency()*1000)/getDisplayedSampleRate()),
|
|
false);
|
|
m_waterfallMarkers.back().m_time = time;
|
|
m_waterfallMarkers.back().m_timeStr = displayScaledF(
|
|
time,
|
|
'f',
|
|
3,
|
|
true);
|
|
|
|
if (m_waterfallMarkers.size() > 1)
|
|
{
|
|
int64_t deltaFrequency = frequency - m_waterfallMarkers.at(0).m_frequency;
|
|
m_waterfallMarkers.back().m_deltaFrequencyStr = displayScaled(
|
|
deltaFrequency,
|
|
'f',
|
|
getPrecision(deltaFrequency/getDisplayedSampleRate()),
|
|
true);
|
|
m_waterfallMarkers.back().m_deltaTimeStr = displayScaledF(
|
|
time - m_waterfallMarkers.at(0).m_time,
|
|
'f',
|
|
3,
|
|
true);
|
|
}
|
|
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportWaterfallMarkersChange());
|
|
}
|
|
doUpdate = true;
|
|
}
|
|
}
|
|
|
|
if (doUpdate) {
|
|
update();
|
|
}
|
|
}
|
|
else if (event->modifiers() & Qt::AltModifier)
|
|
{
|
|
frequencyPan(event);
|
|
}
|
|
else if (event->modifiers() & Qt::ControlModifier)
|
|
{
|
|
if (!m_display3DSpectrogram && pointInWaterfallOrSpectrogram(ep))
|
|
{
|
|
QPointF pWat = ep;
|
|
pWat.rx() = (ep.x()/width() - m_waterfallRect.left()) / m_waterfallRect.width();
|
|
pWat.ry() = (ep.y()/height() - m_waterfallRect.top()) / m_waterfallRect.height();
|
|
float time = m_timeScale.getRangeMin() + pWat.y()*m_timeScale.getRange();
|
|
emit timeSelected(time);
|
|
}
|
|
return;
|
|
}
|
|
else if (m_display3DSpectrogram)
|
|
{
|
|
// Detect click and drag to rotate 3D spectrogram
|
|
if (pointInWaterfallOrSpectrogram(ep))
|
|
{
|
|
m_rotate3DSpectrogram = true;
|
|
m_mousePrevLocalPos = ep;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ((m_markersDisplay & SpectrumSettings::MarkersDisplayAnnotations) &&
|
|
(ep.y() <= m_histogramRect.top()*height() + m_annotationMarkerHeight + 2.0f))
|
|
{
|
|
QPointF pHis;
|
|
pHis.rx() = (ep.x()/width() - m_histogramRect.left()) / m_histogramRect.width();
|
|
qint64 selectedFrequency = m_frequencyScale.getRangeMin() + pHis.x() * m_frequencyScale.getRange();
|
|
bool selected = false;
|
|
|
|
for (auto iMarker = m_visibleAnnotationMarkers.rbegin(); iMarker != m_visibleAnnotationMarkers.rend(); ++iMarker)
|
|
{
|
|
if ((*iMarker)->m_show == SpectrumAnnotationMarker::Hidden) {
|
|
continue;
|
|
}
|
|
|
|
qint64 stopFrequency = (*iMarker)->m_startFrequency +
|
|
((*iMarker)->m_bandwidth == 0 ? m_frequencyScale.getRange()*0.01f : (*iMarker)->m_bandwidth);
|
|
|
|
if (((*iMarker)->m_startFrequency < selectedFrequency) && (selectedFrequency <= stopFrequency) && !selected)
|
|
{
|
|
switch ((*iMarker)->m_show)
|
|
{
|
|
case SpectrumAnnotationMarker::ShowTop:
|
|
(*iMarker)->m_show = SpectrumAnnotationMarker::ShowText;
|
|
break;
|
|
case SpectrumAnnotationMarker::ShowText:
|
|
(*iMarker)->m_show = SpectrumAnnotationMarker::ShowFull;
|
|
break;
|
|
case SpectrumAnnotationMarker::ShowFull:
|
|
(*iMarker)->m_show = SpectrumAnnotationMarker::ShowTop;
|
|
break;
|
|
case SpectrumAnnotationMarker::Hidden:
|
|
break;
|
|
}
|
|
selected = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_cursorState == CSSplitter)
|
|
{
|
|
grabMouse();
|
|
m_cursorState = CSSplitterMoving;
|
|
return;
|
|
}
|
|
else if (m_cursorState == CSChannel)
|
|
{
|
|
grabMouse();
|
|
m_cursorState = CSChannelMoving;
|
|
return;
|
|
}
|
|
else if ((m_cursorState == CSNormal) &&
|
|
(m_channelMarkerStates.size() == 1) &&
|
|
!(event->modifiers() & Qt::ShiftModifier) &&
|
|
!(event->modifiers() & Qt::AltModifier) &&
|
|
!(event->modifiers() & Qt::ControlModifier) &&
|
|
(ep.y() > m_histogramRect.top()*height() + m_annotationMarkerHeight + 2.0f)) // out of annotation selection zone
|
|
{
|
|
grabMouse();
|
|
setCursor(Qt::SizeHorCursor);
|
|
m_cursorState = CSChannelMoving;
|
|
m_cursorChannel = 0;
|
|
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)
|
|
&& m_channelMarkerStates[m_cursorChannel]->m_channelMarker->streamIndexApplies(m_displayStreamIndex))
|
|
{
|
|
m_channelMarkerStates[m_cursorChannel]->m_channelMarker->setCenterFrequencyByCursor(freq);
|
|
channelMarkerChanged();
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::mouseReleaseEvent(QMouseEvent*)
|
|
{
|
|
m_scrollFrequency = false;
|
|
m_pan3DSpectrogram = false;
|
|
m_rotate3DSpectrogram = false;
|
|
m_scaleZ3DSpectrogram = false;
|
|
if (m_cursorState == CSSplitterMoving)
|
|
{
|
|
releaseMouse();
|
|
m_cursorState = CSSplitter;
|
|
}
|
|
else if (m_cursorState == CSChannelMoving)
|
|
{
|
|
releaseMouse();
|
|
m_cursorState = CSChannel;
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::wheelEvent(QWheelEvent *event)
|
|
{
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
|
const QPointF& ep = event->position();
|
|
#else
|
|
const QPointF& ep = event->pos();
|
|
#endif
|
|
if (m_display3DSpectrogram && pointInWaterfallOrSpectrogram(ep))
|
|
{
|
|
// Scale 3D spectrogram when mouse wheel moved
|
|
// Some mice use delta in steps of 120 for 15 degrees
|
|
// for one step of mouse wheel
|
|
// Other mice/trackpads use smaller values
|
|
int delta = event->angleDelta().y();
|
|
if (delta != 0) {
|
|
m_glShaderSpectrogram.verticalAngle(-5.0*delta/120.0);
|
|
}
|
|
repaint(); // Force repaint in case acquisition is stopped
|
|
}
|
|
else
|
|
{
|
|
if (event->modifiers() & Qt::ShiftModifier) {
|
|
channelMarkerMove(event, 100);
|
|
} else if (event->modifiers() & Qt::ControlModifier) {
|
|
channelMarkerMove(event, 10);
|
|
} else {
|
|
channelMarkerMove(event, 1);
|
|
}
|
|
}
|
|
event->accept();
|
|
}
|
|
|
|
void GLSpectrumView::zoomFactor(const QPointF& p, float factor)
|
|
{
|
|
float pwx = (p.x() - m_leftMargin) / (width() - m_leftMargin - m_rightMargin); // x position in window
|
|
|
|
if ((pwx >= 0.0f) && (pwx <= 1.0f))
|
|
{
|
|
// When we zoom, we want the frequency under the cursor to remain the same
|
|
|
|
// Determine frequency at cursor position
|
|
float zoomFreq = m_frequencyScale.getRangeMin() + pwx*m_frequencyScale.getRange();
|
|
|
|
// Calculate current centre frequency
|
|
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);
|
|
|
|
// Calculate what that difference would be if there was no zoom
|
|
float freqDiffZoom1 = freqDiff * m_frequencyZoomFactor;
|
|
|
|
m_frequencyZoomFactor *= factor;
|
|
m_frequencyZoomFactor = std::min(m_frequencyZoomFactor, m_maxFrequencyZoom);
|
|
m_frequencyZoomFactor = std::max(m_frequencyZoomFactor, 1.0f);
|
|
|
|
// Calculate what frequency difference should be at new zoom
|
|
float zoomedFreqDiff = freqDiffZoom1 / m_frequencyZoomFactor;
|
|
// Then calculate what the center frequency should be
|
|
float zoomedCF = zoomFreq + zoomedFreqDiff;
|
|
|
|
// Calculate zoom position which will set the desired center frequency
|
|
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))
|
|
{
|
|
// When we zoom, we want the frequency under the cursor to remain the same
|
|
|
|
// Determine frequency at cursor position
|
|
float zoomFreq = m_frequencyScale.getRangeMin() + pwx*m_frequencyScale.getRange();
|
|
|
|
// Calculate current centre frequency
|
|
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;
|
|
|
|
// Calculate difference from frequency under cursor to centre frequency
|
|
float freqDiff = (currentCF - zoomFreq);
|
|
|
|
// Calculate what that difference would be if there was no zoom
|
|
float freqDiffZoom1 = freqDiff * m_frequencyZoomFactor;
|
|
|
|
if (y > 0) // zoom in
|
|
{
|
|
if (m_frequencyZoomFactor < m_maxFrequencyZoom) {
|
|
m_frequencyZoomFactor += 0.5f;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (m_frequencyZoomFactor > 1.0f) {
|
|
m_frequencyZoomFactor -= 0.5f;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Calculate what frequency difference should be at new zoom
|
|
float zoomedFreqDiff = freqDiffZoom1 / m_frequencyZoomFactor;
|
|
// Then calculate what the center frequency should be
|
|
float zoomedCF = zoomFreq + zoomedFreqDiff;
|
|
|
|
// Calculate zoom position which will set the desired center frequency
|
|
float zoomPos = (zoomedCF - adjCenterFrequency) / adjSampleRate + 0.5;
|
|
zoomPos = std::max(0.0f, zoomPos);
|
|
zoomPos = std::min(1.0f, zoomPos);
|
|
|
|
frequencyZoom(zoomPos);
|
|
}
|
|
else
|
|
{
|
|
float pwyh, pwyw;
|
|
|
|
if (m_invertedWaterfall) // histo on top
|
|
{
|
|
pwyh = (p.y() - m_topMargin) / m_histogramHeight;
|
|
pwyw = (p.y() - m_topMargin - m_histogramHeight - m_frequencyScaleHeight) / m_waterfallHeight;
|
|
}
|
|
else // waterfall on top
|
|
{
|
|
pwyw = (p.y() - m_topMargin) / m_waterfallHeight;
|
|
pwyh = (p.y() - m_topMargin - m_waterfallHeight - m_frequencyScaleHeight) / m_histogramHeight;
|
|
}
|
|
|
|
//qDebug("GLSpectrumView::zoom: pwyh: %f pwyw: %f", pwyh, pwyw);
|
|
|
|
if ((pwyw >= 0.0f) && (pwyw <= 1.0f)) {
|
|
timeZoom(y > 0);
|
|
}
|
|
|
|
if ((pwyh >= 0.0f) && (pwyh <= 1.0f) && !m_linear) {
|
|
powerZoom(pwyh, y > 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::frequencyZoom(float zoomPos)
|
|
{
|
|
m_frequencyZoomPos = zoomPos;
|
|
updateFFTLimits(false);
|
|
m_displayChanged = true;
|
|
}
|
|
|
|
void GLSpectrumView::frequencyPan(QMouseEvent *event)
|
|
{
|
|
if (m_frequencyZoomFactor == 1.0f) {
|
|
return;
|
|
}
|
|
|
|
const QPointF& p = event->pos();
|
|
float pw = (p.x() - m_leftMargin) / (width() - m_leftMargin - m_rightMargin); // position in window
|
|
pw = pw < 0.0f ? 0.0f : pw > 1.0f ? 1.0 : pw;
|
|
float dw = pw - 0.5f;
|
|
m_frequencyZoomPos += dw * (1.0f / m_frequencyZoomFactor);
|
|
float lim = 0.5f / m_frequencyZoomFactor;
|
|
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(false);
|
|
}
|
|
|
|
void GLSpectrumView::timeZoom(bool zoomInElseOut)
|
|
{
|
|
if ((m_fftOverlap == 0) && !zoomInElseOut) {
|
|
return;
|
|
}
|
|
|
|
if (zoomInElseOut && (m_fftOverlap == m_fftSize - 1)) {
|
|
return;
|
|
}
|
|
|
|
m_fftOverlap = m_fftOverlap + (zoomInElseOut ? 1 : -1);
|
|
m_changesPending = true;
|
|
|
|
if (m_messageQueueToGUI) {
|
|
m_messageQueueToGUI->push(new MsgReportFFTOverlap(m_fftOverlap));
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::powerZoom(float pw, bool zoomInElseOut)
|
|
{
|
|
m_powerRange = m_powerRange + (zoomInElseOut ? -2 : 2);
|
|
|
|
if (pw > 2.0/3.0) { // bottom
|
|
m_referenceLevel = m_referenceLevel + (zoomInElseOut ? -2 : 2);
|
|
} else if (pw > 1.0/3.0) { // middle
|
|
m_referenceLevel = m_referenceLevel + (zoomInElseOut ? -1 : 1);
|
|
} // top
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::resetFrequencyZoom()
|
|
{
|
|
m_frequencyZoomFactor = 1.0f;
|
|
m_frequencyZoomPos = 0.5f;
|
|
|
|
updateFFTLimits(false);
|
|
}
|
|
|
|
void GLSpectrumView::updateFFTLimits(bool fftSizeChangedOnly)
|
|
{
|
|
if (!fftSizeChangedOnly)
|
|
{
|
|
if (m_messageQueueToGUI)
|
|
{
|
|
m_messageQueueToGUI->push(GLSpectrumView::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;
|
|
}
|
|
|
|
void GLSpectrumView::setFrequencyZooming(float frequencyZoomFactor, float frequencyZoomPos)
|
|
{
|
|
m_frequencyZoomFactor = frequencyZoomFactor;
|
|
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;
|
|
int64_t centerFrequency;
|
|
|
|
getFrequencyZoom(centerFrequency, frequencySpan);
|
|
m_frequencyScale.setSize(width() - m_leftMargin - m_rightMargin);
|
|
m_frequencyScale.setRange(Unit::Frequency, centerFrequency - frequencySpan / 2.0, centerFrequency + frequencySpan / 2.0);
|
|
m_frequencyScale.setMakeOpposite(m_lsbDisplay);
|
|
}
|
|
|
|
void GLSpectrumView::setPowerScale(int height)
|
|
{
|
|
m_powerScale.setSize(height);
|
|
|
|
if (m_linear)
|
|
{
|
|
Real referenceLevel = m_useCalibration ? m_referenceLevel * m_calibrationGain : m_referenceLevel;
|
|
m_powerScale.setRange(Unit::Scientific, 0.0f, referenceLevel);
|
|
}
|
|
else
|
|
{
|
|
Real referenceLevel = m_useCalibration ? m_referenceLevel + m_calibrationShiftdB : m_referenceLevel;
|
|
m_powerScale.setRange(Unit::Decibel, referenceLevel - m_powerRange, referenceLevel);
|
|
}
|
|
|
|
if (m_powerScale.getScaleWidth() > m_leftMargin) {
|
|
m_leftMargin = m_powerScale.getScaleWidth();
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::getFrequencyZoom(int64_t& centerFrequency, int& frequencySpan)
|
|
{
|
|
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::channelMarkerMove(QWheelEvent *event, int mul)
|
|
{
|
|
for (int i = 0; i < m_channelMarkerStates.size(); ++i)
|
|
{
|
|
if ((m_channelMarkerStates[i]->m_channelMarker->getSourceOrSinkStream() != m_displaySourceOrSink)
|
|
|| !m_channelMarkerStates[i]->m_channelMarker->streamIndexApplies(m_displayStreamIndex))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (m_channelMarkerStates[i]->m_rect.contains(event->position()))
|
|
{
|
|
int freq = m_channelMarkerStates[i]->m_channelMarker->getCenterFrequency();
|
|
|
|
if (event->angleDelta().y() > 0) {
|
|
freq += 10 * mul;
|
|
} else if (event->angleDelta().y() < 0) {
|
|
freq -= 10 * mul;
|
|
}
|
|
|
|
// calculate scale relative cursor position for new frequency
|
|
float x_pos = m_frequencyScale.getPosFromValue(getDisplayedCenterFrequency() + freq);
|
|
|
|
if ((x_pos >= 0.0) && (x_pos < m_frequencyScale.getSize())) // cursor must be in scale
|
|
{
|
|
m_channelMarkerStates[i]->m_channelMarker->setCenterFrequencyByCursor(freq);
|
|
m_channelMarkerStates[i]->m_channelMarker->setCenterFrequency(freq);
|
|
|
|
// cursor follow-up
|
|
int xd = x_pos + m_leftMargin;
|
|
QCursor c = cursor();
|
|
QPoint cp_a = c.pos();
|
|
QPoint cp_w = mapFromGlobal(cp_a);
|
|
cp_w.setX(xd);
|
|
cp_a = mapToGlobal(cp_w);
|
|
c.setPos(cp_a);
|
|
setCursor(c);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
zoom(event->position(), event->angleDelta().y());
|
|
}
|
|
|
|
// Return if specified point is within the bounds of the waterfall / 3D spectrogram screen area
|
|
bool GLSpectrumView::pointInWaterfallOrSpectrogram(const QPointF &point) const
|
|
{
|
|
// m_waterfallRect is normalised to [0,1]
|
|
QPointF pWat = point;
|
|
pWat.rx() = (point.x()/width() - m_waterfallRect.left()) / m_waterfallRect.width();
|
|
pWat.ry() = (point.y()/height() - m_waterfallRect.top()) / m_waterfallRect.height();
|
|
|
|
return (pWat.x() >= 0) && (pWat.x() <= 1) && (pWat.y() >= 0) && (pWat.y() <= 1);
|
|
}
|
|
|
|
// Return if specified point is within the bounds of the histogram screen area
|
|
bool GLSpectrumView::pointInHistogram(const QPointF &point) const
|
|
{
|
|
// m_histogramRect is normalised to [0,1]
|
|
QPointF p = point;
|
|
p.rx() = (point.x()/width() - m_histogramRect.left()) / m_histogramRect.width();
|
|
p.ry() = (point.y()/height() - m_histogramRect.top()) / m_histogramRect.height();
|
|
|
|
return (p.x() >= 0) && (p.x() <= 1) && (p.y() >= 0) && (p.y() <= 1);
|
|
}
|
|
|
|
void GLSpectrumView::enterEvent(EnterEventType* event)
|
|
{
|
|
m_mouseInside = true;
|
|
update();
|
|
QOpenGLWidget::enterEvent(event);
|
|
}
|
|
|
|
void GLSpectrumView::leaveEvent(QEvent* event)
|
|
{
|
|
m_mouseInside = false;
|
|
update();
|
|
QOpenGLWidget::leaveEvent(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;
|
|
update();
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::channelMarkerChanged()
|
|
{
|
|
QMutexLocker mutexLocker(&m_mutex);
|
|
m_changesPending = true;
|
|
update();
|
|
}
|
|
|
|
void GLSpectrumView::channelMarkerDestroyed(QObject* object)
|
|
{
|
|
removeChannelMarker((ChannelMarker*)object);
|
|
}
|
|
|
|
void GLSpectrumView::setWaterfallShare(Real waterfallShare)
|
|
{
|
|
QMutexLocker mutexLocker(&m_mutex);
|
|
|
|
if (waterfallShare < 0.1f) {
|
|
m_waterfallShare = 0.1f;
|
|
} else if (waterfallShare > 0.8f) {
|
|
m_waterfallShare = 0.8f;
|
|
} else {
|
|
m_waterfallShare = waterfallShare;
|
|
}
|
|
|
|
m_changesPending = true;
|
|
}
|
|
|
|
void GLSpectrumView::setFPSPeriodMs(int fpsPeriodMs)
|
|
{
|
|
if (fpsPeriodMs == 0)
|
|
{
|
|
disconnect(&m_timer, SIGNAL(timeout()), this, SLOT(tick()));
|
|
m_timer.stop();
|
|
}
|
|
else
|
|
{
|
|
connect(&m_timer, SIGNAL(timeout()), this, SLOT(tick()));
|
|
m_timer.start(fpsPeriodMs);
|
|
}
|
|
|
|
m_fpsPeriodMs = fpsPeriodMs;
|
|
}
|
|
|
|
void GLSpectrumView::cleanup()
|
|
{
|
|
//makeCurrent();
|
|
m_glShaderSimple.cleanup();
|
|
m_glShaderFrequencyScale.cleanup();
|
|
m_glShaderHistogram.cleanup();
|
|
m_glShaderLeftScale.cleanup();
|
|
m_glShaderWaterfall.cleanup();
|
|
m_glShaderTextOverlay.cleanup();
|
|
m_glShaderInfo.cleanup();
|
|
m_glShaderSpectrogram.cleanup();
|
|
m_glShaderSpectrogramTimeScale.cleanup();
|
|
m_glShaderSpectrogramPowerScale.cleanup();
|
|
//doneCurrent();
|
|
}
|
|
|
|
// Display number with full precision, group separators and eng. unit suffixes
|
|
// E.g:
|
|
// -1.505,123,304G
|
|
// 456.034,123M
|
|
// 300.345k
|
|
// 789
|
|
QString GLSpectrumView::displayFull(int64_t value)
|
|
{
|
|
if (value == 0) {
|
|
return "0";
|
|
}
|
|
int64_t absValue = std::abs(value);
|
|
|
|
QString digits = QString::number(absValue);
|
|
int cnt = digits.size();
|
|
|
|
QString point = QLocale::system().decimalPoint();
|
|
QString group = QLocale::system().groupSeparator();
|
|
int i;
|
|
for (i = cnt - 3; i >= 4; i -= 3)
|
|
{
|
|
digits = digits.insert(i, group);
|
|
}
|
|
if (absValue >= 1000) {
|
|
digits = digits.insert(i, point);
|
|
}
|
|
if (cnt > 9) {
|
|
digits = digits.append("G");
|
|
} else if (cnt > 6) {
|
|
digits = digits.append("M");
|
|
} else if (cnt > 3) {
|
|
digits = digits.append("k");
|
|
}
|
|
if (value < 0) {
|
|
digits = digits.insert(0, "-");
|
|
}
|
|
|
|
return digits;
|
|
}
|
|
|
|
QString GLSpectrumView::displayScaled(int64_t value, char type, int precision, bool showMult)
|
|
{
|
|
int64_t posValue = (value < 0) ? -value : value;
|
|
|
|
if (posValue < 1000) {
|
|
return tr("%1").arg(QString::number(value, type, precision));
|
|
} else if (posValue < 1000000) {
|
|
return tr("%1%2").arg(QString::number(value / 1000.0, type, precision)).arg(showMult ? "k" : "");
|
|
} else if (posValue < 1000000000) {
|
|
return tr("%1%2").arg(QString::number(value / 1000000.0, type, precision)).arg(showMult ? "M" : "");
|
|
} else if (posValue < 1000000000000) {
|
|
return tr("%1%2").arg(QString::number(value / 1000000000.0, type, precision)).arg(showMult ? "G" : "");
|
|
} else {
|
|
return tr("%1").arg(QString::number(value, 'e', precision));
|
|
}
|
|
}
|
|
|
|
QString GLSpectrumView::displayPower(float value, char type, int precision)
|
|
{
|
|
return tr("%1").arg(QString::number(value, type, precision));
|
|
}
|
|
|
|
QString GLSpectrumView::displayScaledF(float value, char type, int precision, bool showMult)
|
|
{
|
|
float posValue = (value < 0) ? -value : value;
|
|
|
|
if (posValue == 0)
|
|
{
|
|
return tr("%1").arg(QString::number(value, 'f', precision));
|
|
}
|
|
else if (posValue < 1)
|
|
{
|
|
if (posValue > 0.001) {
|
|
return tr("%1%2").arg(QString::number(value * 1000.0, type, precision)).arg(showMult ? "m" : "");
|
|
} else if (posValue > 0.000001) {
|
|
return tr("%1%2").arg(QString::number(value * 1000000.0, type, precision)).arg(showMult ? "u" : "");
|
|
} else if (posValue > 1e-9) {
|
|
return tr("%1%2").arg(QString::number(value * 1e9, type, precision)).arg(showMult ? "n" : "");
|
|
} else if (posValue > 1e-12) {
|
|
return tr("%1%2").arg(QString::number(value * 1e12, type, precision)).arg(showMult ? "p" : "");
|
|
} else {
|
|
return tr("%1").arg(QString::number(value, 'e', precision));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (posValue < 1e3) {
|
|
return tr("%1").arg(QString::number(value, type, precision));
|
|
} else if (posValue < 1e6) {
|
|
return tr("%1%2").arg(QString::number(value / 1000.0, type, precision)).arg(showMult ? "k" : "");
|
|
} else if (posValue < 1e9) {
|
|
return tr("%1%2").arg(QString::number(value / 1000000.0, type, precision)).arg(showMult ? "M" : "");
|
|
} else if (posValue < 1e12) {
|
|
return tr("%1%2").arg(QString::number(value / 1000000000.0, type, precision)).arg(showMult ? "G" : "");
|
|
} else {
|
|
return tr("%1").arg(QString::number(value, 'e', precision));
|
|
}
|
|
}
|
|
}
|
|
|
|
int GLSpectrumView::getPrecision(int value)
|
|
{
|
|
int posValue = (value < 0) ? -value : value;
|
|
|
|
if (posValue < 1000) {
|
|
return 3;
|
|
} else if (posValue < 10000) {
|
|
return 4;
|
|
} else if (posValue < 100000) {
|
|
return 5;
|
|
} else {
|
|
return 6;
|
|
}
|
|
}
|
|
|
|
// Draw text right justified in top info bar - currently unused
|
|
void GLSpectrumView::drawTextRight(const QString &text, const QString &value, const QString &max, const QString &units)
|
|
{
|
|
drawTextsRight({text}, {value}, {max}, {units});
|
|
}
|
|
|
|
void GLSpectrumView::drawTextsRight(const QStringList &text, const QStringList &value, const QStringList &max, const QStringList &units)
|
|
{
|
|
QFontMetrics fm(font());
|
|
|
|
m_infoPixmap.fill(Qt::transparent);
|
|
|
|
QPainter painter(&m_infoPixmap);
|
|
painter.setPen(Qt::NoPen);
|
|
painter.setBrush(Qt::black);
|
|
painter.setBrush(Qt::transparent);
|
|
painter.drawRect(m_leftMargin, 0, width() - m_leftMargin, m_infoHeight);
|
|
painter.setPen(QColor(0xf0, 0xf0, 0xff));
|
|
painter.setFont(font());
|
|
|
|
int x = width() - m_rightMargin;
|
|
int y = fm.height() + fm.ascent() / 2 - 2;
|
|
int textWidth, maxWidth;
|
|
for (int i = text.length() - 1; i >= 0; i--)
|
|
{
|
|
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(text[i]);
|
|
painter.drawText(QPointF(x - textWidth, y), text[i]);
|
|
x -= textWidth;
|
|
}
|
|
}
|
|
|
|
m_glShaderTextOverlay.initTexture(m_infoPixmap.toImage());
|
|
|
|
GLfloat vtx1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
GLfloat tex1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0
|
|
};
|
|
|
|
m_glShaderTextOverlay.drawSurface(m_glInfoBoxMatrix, tex1, vtx1, 4);
|
|
}
|
|
|
|
void GLSpectrumView::drawTextOverlayCentered (
|
|
const QString &text,
|
|
const QColor &color,
|
|
const QFont& font,
|
|
float shiftX,
|
|
float shiftY,
|
|
const QRectF &glRect)
|
|
{
|
|
if (text.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QFontMetricsF metrics(font);
|
|
QRectF textRect = metrics.boundingRect(text);
|
|
QRectF overlayRect(0, 0, textRect.width() * 1.05f + 4.0f, textRect.height());
|
|
QPixmap channelOverlayPixmap = QPixmap(overlayRect.width(), overlayRect.height());
|
|
channelOverlayPixmap.fill(Qt::transparent);
|
|
QPainter painter(&channelOverlayPixmap);
|
|
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing, false);
|
|
painter.fillRect(overlayRect, QColor(0, 0, 0, 0x80));
|
|
QColor textColor(color);
|
|
textColor.setAlpha(0xC0);
|
|
painter.setPen(textColor);
|
|
painter.setFont(font);
|
|
painter.drawText(QPointF(2.0f, overlayRect.height() - 4.0f), text);
|
|
painter.end();
|
|
|
|
m_glShaderTextOverlay.initTexture(channelOverlayPixmap.toImage());
|
|
|
|
{
|
|
GLfloat vtx1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0};
|
|
GLfloat tex1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0};
|
|
|
|
float rectX = glRect.x() + shiftX - ((overlayRect.width()/2)/width());
|
|
float rectY = glRect.y() + shiftY + (4.0f / height()) - ((overlayRect.height()+5)/height());
|
|
float rectW = overlayRect.width() / (float) width();
|
|
float rectH = overlayRect.height() / (float) height();
|
|
|
|
QMatrix4x4 mat;
|
|
mat.setToIdentity();
|
|
mat.translate(-1.0f + 2.0f * rectX, 1.0f - 2.0f * rectY);
|
|
mat.scale(2.0f * rectW, -2.0f * rectH);
|
|
m_glShaderTextOverlay.drawSurface(mat, tex1, vtx1, 4);
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::drawTextOverlay(
|
|
const QString &text,
|
|
const QColor &color,
|
|
const QFont& font,
|
|
float shiftX,
|
|
float shiftY,
|
|
bool leftHalf,
|
|
bool topHalf,
|
|
const QRectF &glRect)
|
|
{
|
|
if (text.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QFontMetricsF metrics(font);
|
|
QRectF textRect = metrics.boundingRect(text);
|
|
QRectF overlayRect(0, 0, textRect.width() * 1.05f + 4.0f, textRect.height());
|
|
QPixmap channelOverlayPixmap = QPixmap(overlayRect.width(), overlayRect.height());
|
|
channelOverlayPixmap.fill(Qt::transparent);
|
|
QPainter painter(&channelOverlayPixmap);
|
|
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing, false);
|
|
painter.fillRect(overlayRect, QColor(0, 0, 0, 0x80));
|
|
QColor textColor(color);
|
|
textColor.setAlpha(0xC0);
|
|
painter.setPen(textColor);
|
|
painter.setFont(font);
|
|
painter.drawText(QPointF(2.0f, overlayRect.height() - 4.0f), text);
|
|
painter.end();
|
|
|
|
m_glShaderTextOverlay.initTexture(channelOverlayPixmap.toImage());
|
|
|
|
{
|
|
GLfloat vtx1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0};
|
|
GLfloat tex1[] = {
|
|
0, 1,
|
|
1, 1,
|
|
1, 0,
|
|
0, 0};
|
|
|
|
// float shiftX = glRect.width() - ((overlayRect.width() + 4.0f) / width());
|
|
// float shiftY = 4.0f / height();
|
|
float rectX = glRect.x() + shiftX - (leftHalf ? 0 : (overlayRect.width()+1)/width());
|
|
float rectY = glRect.y() + shiftY + (4.0f / height()) - (topHalf ? 0 : (overlayRect.height()+5)/height());
|
|
float rectW = overlayRect.width() / (float) width();
|
|
float rectH = overlayRect.height() / (float) height();
|
|
|
|
QMatrix4x4 mat;
|
|
mat.setToIdentity();
|
|
mat.translate(-1.0f + 2.0f * rectX, 1.0f - 2.0f * rectY);
|
|
mat.scale(2.0f * rectW, -2.0f * rectH);
|
|
m_glShaderTextOverlay.drawSurface(mat, tex1, vtx1, 4);
|
|
}
|
|
}
|
|
|
|
void GLSpectrumView::formatTextInfo(QString& info)
|
|
{
|
|
QString spacing = " ";
|
|
|
|
if (m_useCalibration) {
|
|
info.append(tr("CAL:%1dB%2").arg(QString::number(m_calibrationShiftdB, 'f', 1)).arg(spacing));
|
|
}
|
|
|
|
if (m_frequencyZoomFactor != 1.0f) {
|
|
info.append(tr("%1x%2").arg(QString::number(m_frequencyZoomFactor, 'f', 1)).arg(spacing));
|
|
}
|
|
|
|
if (getDisplayedSampleRate() == 0)
|
|
{
|
|
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%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)
|
|
{
|
|
if (event->type() == QEvent::KeyPress)
|
|
{
|
|
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
|
|
switch (keyEvent->key())
|
|
{
|
|
case Qt::Key_Up:
|
|
if (keyEvent->modifiers() & Qt::ShiftModifier) {
|
|
m_glShaderSpectrogram.lightRotateX(-5.0f);
|
|
} else if (keyEvent->modifiers() & Qt::AltModifier) {
|
|
m_glShaderSpectrogram.lightTranslateY(0.05);
|
|
} else if (keyEvent->modifiers() & Qt::ControlModifier) {
|
|
m_glShaderSpectrogram.translateY(0.05);
|
|
} else {
|
|
m_glShaderSpectrogram.rotateX(-5.0f);
|
|
}
|
|
break;
|
|
case Qt::Key_Down:
|
|
if (keyEvent->modifiers() & Qt::ShiftModifier) {
|
|
m_glShaderSpectrogram.lightRotateX(5.0f);
|
|
} else if (keyEvent->modifiers() & Qt::AltModifier) {
|
|
m_glShaderSpectrogram.lightTranslateY(-0.05);
|
|
} else if (keyEvent->modifiers() & Qt::ControlModifier) {
|
|
m_glShaderSpectrogram.translateY(-0.05);
|
|
} else {
|
|
m_glShaderSpectrogram.rotateX(5.0f);
|
|
}
|
|
break;
|
|
case Qt::Key_Left:
|
|
if (keyEvent->modifiers() & Qt::ShiftModifier) {
|
|
m_glShaderSpectrogram.lightRotateZ(5.0f);
|
|
} else if (keyEvent->modifiers() & Qt::AltModifier) {
|
|
m_glShaderSpectrogram.lightTranslateX(-0.05);
|
|
} else if (keyEvent->modifiers() & Qt::ControlModifier) {
|
|
m_glShaderSpectrogram.translateX(-0.05);
|
|
} else {
|
|
m_glShaderSpectrogram.rotateZ(5.0f);
|
|
}
|
|
break;
|
|
case Qt::Key_Right:
|
|
if (keyEvent->modifiers() & Qt::ShiftModifier) {
|
|
m_glShaderSpectrogram.lightRotateZ(-5.0f);
|
|
} else if (keyEvent->modifiers() & Qt::AltModifier) {
|
|
m_glShaderSpectrogram.lightTranslateX(0.05);
|
|
} else if (keyEvent->modifiers() & Qt::ControlModifier) {
|
|
m_glShaderSpectrogram.translateX(0.05);
|
|
} else {
|
|
m_glShaderSpectrogram.rotateZ(-5.0f);
|
|
}
|
|
break;
|
|
case Qt::Key_Equal: // So you don't need to press shift
|
|
case Qt::Key_Plus:
|
|
if (keyEvent->modifiers() & Qt::ControlModifier) {
|
|
m_glShaderSpectrogram.userScaleZ(1.1f);
|
|
} else {
|
|
m_glShaderSpectrogram.verticalAngle(-1.0f);
|
|
}
|
|
break;
|
|
case Qt::Key_Minus:
|
|
if (keyEvent->modifiers() & Qt::ControlModifier) {
|
|
m_glShaderSpectrogram.userScaleZ(0.9f);
|
|
} else {
|
|
m_glShaderSpectrogram.verticalAngle(1.0f);
|
|
}
|
|
break;
|
|
case Qt::Key_R:
|
|
m_glShaderSpectrogram.reset();
|
|
break;
|
|
case Qt::Key_F:
|
|
// Project straight down and scale to view, so it's a bit like 2D
|
|
m_glShaderSpectrogram.reset();
|
|
m_glShaderSpectrogram.rotateX(45.0f);
|
|
m_glShaderSpectrogram.verticalAngle(-9.0f);
|
|
m_glShaderSpectrogram.userScaleZ(0.0f);
|
|
break;
|
|
}
|
|
repaint(); // Force repaint in case acquisition is stopped
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
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<Real>& 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);
|
|
}
|
|
}
|
|
}
|