diff --git a/doc/img/Spectrum_Measurement_AdjChannelPower.png b/doc/img/Spectrum_Measurement_AdjChannelPower.png new file mode 100644 index 000000000..d55489a04 Binary files /dev/null and b/doc/img/Spectrum_Measurement_AdjChannelPower.png differ diff --git a/doc/img/Spectrum_Measurement_ChannelPower.png b/doc/img/Spectrum_Measurement_ChannelPower.png new file mode 100644 index 000000000..bbf837d08 Binary files /dev/null and b/doc/img/Spectrum_Measurement_ChannelPower.png differ diff --git a/doc/img/Spectrum_Measurement_Peak.png b/doc/img/Spectrum_Measurement_Peak.png new file mode 100644 index 000000000..903c45995 Binary files /dev/null and b/doc/img/Spectrum_Measurement_Peak.png differ diff --git a/doc/img/Spectrum_Measurement_SFDR.png b/doc/img/Spectrum_Measurement_SFDR.png new file mode 100644 index 000000000..f73a811ec Binary files /dev/null and b/doc/img/Spectrum_Measurement_SFDR.png differ diff --git a/doc/img/Spectrum_Measurement_SNR.png b/doc/img/Spectrum_Measurement_SNR.png new file mode 100644 index 000000000..41e0e3a3d Binary files /dev/null and b/doc/img/Spectrum_Measurement_SNR.png differ diff --git a/sdrbase/dsp/spectrumsettings.cpp b/sdrbase/dsp/spectrumsettings.cpp index f7b2ff96f..1dc4ce700 100644 --- a/sdrbase/dsp/spectrumsettings.cpp +++ b/sdrbase/dsp/spectrumsettings.cpp @@ -68,6 +68,12 @@ void SpectrumSettings::resetToDefaults() m_3DSpectrogramStyle = Outline; m_colorMap = "Angel"; m_spectrumStyle = Line; + m_measurement = MeasurementNone; + m_measurementBandwidth = 10000; + m_measurementChSpacing = 10000; + m_measurementAdjChBandwidth = 10000; + m_measurementHarmonics = 5; + m_measurementHighlight = true; } QByteArray SpectrumSettings::serialize() const @@ -107,6 +113,13 @@ QByteArray SpectrumSettings::serialize() const s.writeS32(32, (int) m_3DSpectrogramStyle); s.writeString(33, m_colorMap); s.writeS32(34, (int) m_spectrumStyle); + s.writeS32(35, (int) m_measurement); + s.writeS32(36, m_measurementBandwidth); + s.writeS32(37, m_measurementChSpacing); + s.writeS32(38, m_measurementAdjChBandwidth); + s.writeS32(39, m_measurementHarmonics); + // 41, 42 used below + s.writeBool(42, m_measurementHighlight); s.writeS32(100, m_histogramMarkers.size()); for (int i = 0; i < m_histogramMarkers.size(); i++) { @@ -208,6 +221,12 @@ bool SpectrumSettings::deserialize(const QByteArray& data) d.readS32(32, (int*)&m_3DSpectrogramStyle, (int)Outline); d.readString(33, &m_colorMap, "Angel"); d.readS32(34, (int*)&m_spectrumStyle, (int)Line); + d.readS32(35, (int*)&m_measurement, (int)MeasurementNone); + d.readS32(36, &m_measurementBandwidth, 10000); + d.readS32(37, &m_measurementChSpacing, 10000); + d.readS32(38, &m_measurementAdjChBandwidth, 10000); + d.readS32(39, &m_measurementHarmonics, 5); + d.readBool(42, &m_measurementHighlight, true); int histogramMarkersSize; d.readS32(100, &histogramMarkersSize, 0); diff --git a/sdrbase/dsp/spectrumsettings.h b/sdrbase/dsp/spectrumsettings.h index c1a9cdace..eec028ee0 100644 --- a/sdrbase/dsp/spectrumsettings.h +++ b/sdrbase/dsp/spectrumsettings.h @@ -70,6 +70,20 @@ public: Gradient }; + enum Measurement + { + MeasurementNone, + MeasurementPeak, + MeasurementChannelPower, + MeasurementAdjacentChannelPower, + MeasurementSNR, + MeasurementSNFR, + MeasurementTHD, + MeasurementTHDPN, + MeasurementSINAD, + MeasurementSFDR + }; + int m_fftSize; int m_fftOverlap; FFTWindow::Function m_fftWindow; @@ -108,6 +122,12 @@ public: SpectrogramStyle m_3DSpectrogramStyle; QString m_colorMap; SpectrumStyle m_spectrumStyle; + Measurement m_measurement; + int m_measurementBandwidth; + int m_measurementChSpacing; + int m_measurementAdjChBandwidth; + int m_measurementHarmonics; + bool m_measurementHighlight; static const int m_log2FFTSizeMin = 6; // 64 static const int m_log2FFTSizeMax = 15; // 32k diff --git a/sdrgui/gui/glspectrum.cpp b/sdrgui/gui/glspectrum.cpp index 885610884..210ea7bb3 100644 --- a/sdrgui/gui/glspectrum.cpp +++ b/sdrgui/gui/glspectrum.cpp @@ -108,7 +108,13 @@ GLSpectrum::GLSpectrum(QWidget* parent) : m_calibrationInterpMode(SpectrumSettings::CalibInterpLinear), m_messageQueueToGUI(nullptr), m_openGLLogger(nullptr), - m_isDeviceSpectrum(false) + m_isDeviceSpectrum(false), + m_measurement(SpectrumSettings::MeasurementNone), + m_measurementBandwidth(10000), + m_measurementChSpacing(10000), + m_measurementAdjChBandwidth(10000), + m_measurementHarmonics(5), + m_measurementHighlight(true) { // Enable multisampling anti-aliasing (MSAA) int multisamples = MainCore::instance()->getSettings().getMultisampling(); @@ -485,6 +491,22 @@ void GLSpectrum::setUseCalibration(bool useCalibration) update(); } +void GLSpectrum::setMeasurementParams(SpectrumSettings::Measurement measurement, + int bandwidth, int chSpacing, int adjChBandwidth, + int harmonics, bool highlight) +{ + m_mutex.lock(); + m_measurement = measurement; + m_measurementBandwidth = bandwidth; + m_measurementChSpacing = chSpacing; + m_measurementAdjChBandwidth = adjChBandwidth; + m_measurementHarmonics = harmonics; + m_measurementHighlight = highlight; + m_changesPending = true; + m_mutex.unlock(); + update(); +} + void GLSpectrum::addChannelMarker(ChannelMarker* channelMarker) { m_mutex.lock(); @@ -1650,20 +1672,93 @@ void GLSpectrum::paintGL() m_glShaderInfo.drawSurface(m_glInfoBoxMatrix, tex1, vtx1, 4); } - // Find and display peak in info line + if (m_currentSpectrum) { - if (m_currentSpectrum) + switch (m_measurement) { - float power, frequency; - - findPeak(power, frequency); - drawPeakText(power, (int64_t)frequency); + case SpectrumSettings::MeasurementPeak: + measurePeak(); + break; + case SpectrumSettings::MeasurementChannelPower: + measureChannelPower(); + break; + case SpectrumSettings::MeasurementAdjacentChannelPower: + measureAdjacentChannelPower(); + break; + case SpectrumSettings::MeasurementSNR: + case SpectrumSettings::MeasurementSNFR: + case SpectrumSettings::MeasurementTHD: + case SpectrumSettings::MeasurementTHDPN: + case SpectrumSettings::MeasurementSINAD: + measureSNR(); + break; + case SpectrumSettings::MeasurementSFDR: + measureSFDR(); + break; + default: + break; } } m_mutex.unlock(); } +// Hightlight power band for SFDR +void GLSpectrum::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); +} + +// Hightlight bandwidth being measured +void GLSpectrum::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); +} + +// Hightlight peak being measured. Note that the peak isn't always at the center +void GLSpectrum::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 GLSpectrum::drawSpectrumMarkers() { if (!m_currentSpectrum) { @@ -1958,6 +2053,369 @@ void GLSpectrum::drawAnnotationMarkers() } } +// Find and display peak in info line +void GLSpectrum::measurePeak() +{ + float power, frequency; + + findPeak(power, frequency); + + drawTextsRight( + {"Peak: ", ""}, + { + displayPower(power, m_linear ? 'e' : 'f', m_linear ? 3 : 1), + displayFull(frequency) + }, + {m_peakPowerMaxStr, m_peakFrequencyMaxStr}, + {m_peakPowerUnits, "Hz"} + ); +} + +// Calculate and display channel power +void GLSpectrum::measureChannelPower() +{ + float power; + + power = calcChannelPower(m_centerFrequency, m_measurementBandwidth); + drawTextRight("Power: ", QString::number(power, 'f', 1), "-120.0", "dB"); + if (m_measurementHighlight) { + drawBandwidthMarkers(m_centerFrequency, m_measurementBandwidth, m_measurementLightMarkerColor); + } +} + +// Calculate and display channel power and adjacent channel power +void GLSpectrum::measureAdjacentChannelPower() +{ + float power, powerLeft, powerRight; + + power = calcChannelPower(m_centerFrequency, m_measurementBandwidth); + powerLeft = calcChannelPower(m_centerFrequency - m_measurementChSpacing, m_measurementAdjChBandwidth); + powerRight = calcChannelPower(m_centerFrequency + m_measurementChSpacing, m_measurementAdjChBandwidth); + + float leftDiff = powerLeft - power; + float rightDiff = powerRight - power; + + drawTextsRight( + {"L: ", "", " C: ", " R: ", ""}, + { QString::number(powerLeft, 'f', 1), + QString::number(leftDiff, 'f', 1), + QString::number(power, 'f', 1), + QString::number(powerRight, 'f', 1), + QString::number(rightDiff, 'f', 1) + }, + {"-120.0", "-120.0", "-120.0", "-120.0", "-120.0"}, + {"dB", "dBc", "dB", "dB", "dBc"} + ); + + if (m_measurementHighlight) + { + drawBandwidthMarkers(m_centerFrequency, m_measurementBandwidth, m_measurementLightMarkerColor); + drawBandwidthMarkers(m_centerFrequency - m_measurementChSpacing, m_measurementAdjChBandwidth, m_measurementDarkMarkerColor); + drawBandwidthMarkers(m_centerFrequency + m_measurementChSpacing, m_measurementAdjChBandwidth, m_measurementDarkMarkerColor); + } +} + +const QVector4D GLSpectrum::m_measurementLightMarkerColor = QVector4D(0.5f, 0.5f, 0.5f, 0.4f); +const QVector4D GLSpectrum::m_measurementDarkMarkerColor = QVector4D(0.5f, 0.5f, 0.5f, 0.3f); + +// Find the width of a peak, by seaching in either direction until +// power is no longer falling +void GLSpectrum::peakWidth(int center, int &left, int &right, int maxLeft, int maxRight) const +{ + float prevLeft = m_currentSpectrum[center]; + float prevRight = m_currentSpectrum[center]; + left = center - 1; + right = center + 1; + while ((left > maxLeft) && (m_currentSpectrum[left] < prevLeft) && (right < maxRight) && (m_currentSpectrum[right] < prevRight)) + { + prevLeft = m_currentSpectrum[left]; + left--; + prevRight = m_currentSpectrum[right]; + right++; + } +} + +int GLSpectrum::findPeakBin() const +{ + int bin; + float power; + + bin = 0; + power = m_currentSpectrum[0]; + for (int i = 1; i < m_nbBins; i++) + { + if (m_currentSpectrum[i] > power) + { + power = m_currentSpectrum[i]; + bin = i; + } + } + return bin; +} + +float GLSpectrum::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 GLSpectrum::frequencyToBin(int64_t frequency) const +{ + float rbw = m_sampleRate / (float)m_fftSize; + return (frequency - m_frequencyScale.getRangeMin()) / rbw; +} + +int64_t GLSpectrum::binToFrequency(int bin) const +{ + float rbw = m_sampleRate / (float)m_fftSize; + return m_frequencyScale.getRangeMin() + bin * rbw; +} + +// Find a peak and measure SNR / THD / SINAD +void GLSpectrum::measureSNR() +{ + // Find bin with max peak - that will be our signal + int sig = findPeakBin(); + int sigLeft, sigRight; + peakWidth(sig, sigLeft, sigRight, 0, m_nbBins); + int sigBins = sigRight - sigLeft - 1; + int binsLeft = sig - sigLeft; + int binsRight = sigRight - sig; + + // Highlight the signal + float hzPerBin = m_sampleRate / (float) m_fftSize; + float sigFreq = binToFrequency(sig); + float sigBW = sigBins * hzPerBin; + if (m_measurementHighlight) { + drawPeakMarkers(binToFrequency(sigLeft+1), binToFrequency(sigRight-1), m_measurementLightMarkerColor); + } + + // Find the harmonics and highlight them + QList hBinsLeft; + QList hBinsRight; + QList 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 (m_currentSpectrum[hBin-1] > m_currentSpectrum[hBin]) { + hBin--; + } else if (m_currentSpectrum[hBin+1] > m_currentSpectrum[hBin]) { + hBin++; + } + hFreq = binToFrequency(hBin); + int hLeft, hRight; + peakWidth(hBin, hLeft, hRight, hBin - binsLeft, hBin + binsRight); + int hBins = hRight - hLeft - 1; + if (m_measurementHighlight) { + 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 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 = m_currentSpectrum[i] * gain; + } else { + power = CalcDb::powerFromdB(m_currentSpectrum[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; + } + + switch (m_measurement) + { + case SpectrumSettings::MeasurementSNR: + { + // Calculate SNR in dB over full bandwidth + float snr = CalcDb::dbPower(sigPower / noisePower); + drawTextRight("SNR: ", QString::number(snr, 'f', 1), "100.0", "dB"); + break; + } + case SpectrumSettings::MeasurementSNFR: + { + // Calculate SNR, where noise is median of noise summed over signal b/w + float snfr = CalcDb::dbPower(sigPower / inBandNoise); + drawTextRight("SNFR: ", QString::number(snfr, 'f', 1), "100.0", "dB"); + break; + } + case SpectrumSettings::MeasurementTHD: + { + // Calculate THD - Total harmonic distortion + float thd = harmonicPower / sigPower; + float thdDB = CalcDb::dbPower(thd); + drawTextRight("THD: ", QString::number(thdDB, 'f', 1), "-120.0", "dB"); + break; + } + case SpectrumSettings::MeasurementTHDPN: + { + // Calculate THD+N - Total harmonic distortion plus noise + float thdpn = CalcDb::dbPower((harmonicPower + noisePower) / sigPower); + drawTextRight("THD+N: ", QString::number(thdpn, 'f', 1), "-120.0", "dB"); + break; + } + case SpectrumSettings::MeasurementSINAD: + { + // Calculate SINAD - Signal to noise and distotion ratio (Should be -THD+N) + float sinad = CalcDb::dbPower((sigPower + harmonicPower + noisePower) / (harmonicPower + noisePower)); + drawTextRight("SINAD: ", QString::number(sinad, 'f', 1), "120.0", "dB"); + break; + } + default: + break; + } +} + +void GLSpectrum::measureSFDR() +{ + // Find first peak which is our signal + int peakBin = findPeakBin(); + int peakLeft, peakRight; + peakWidth(peakBin, peakLeft, peakRight, 0, m_nbBins); + + // Find next largest peak, which is the spur + int nextPeakBin = -1; + float nextPeakPower = -std::numeric_limits::max(); + for (int i = 0; i < m_nbBins; i++) + { + if ((i < peakLeft) || (i > peakRight)) + { + if (m_currentSpectrum[i] > nextPeakPower) + { + nextPeakBin = i; + nextPeakPower = m_currentSpectrum[i]; + } + } + } + if (nextPeakBin != -1) + { + // Calculate SFDR in dB from difference between two peaks + float peakPower = calPower(m_currentSpectrum[peakBin]); + float nextPeakPower = calPower(m_currentSpectrum[nextPeakBin]); + float peakPowerDB = CalcDb::dbPower(peakPower); + float nextPeakPowerDB = CalcDb::dbPower(nextPeakPower); + float sfdr = peakPowerDB - nextPeakPowerDB; + + // Display + drawTextRight("SFDR: ", QString::number(sfdr, 'f', 1), "100.0", "dB"); + if (m_measurementHighlight) + { + if (m_linear) { + drawPowerBandMarkers(peakPower, nextPeakPower, m_measurementLightMarkerColor); + } else { + drawPowerBandMarkers(peakPowerDB, nextPeakPowerDB, m_measurementLightMarkerColor); + } + } + } +} + +// Find power and frequency of max peak in current spectrum +void GLSpectrum::findPeak(float &power, float &frequency) const +{ + int bin; + + bin = 0; + power = m_currentSpectrum[0]; + for (int i = 1; i < m_nbBins; i++) + { + if (m_currentSpectrum[i] > power) + { + power = m_currentSpectrum[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 GLSpectrum::calcChannelPower(int64_t centerFrequency, int channelBandwidth) const +{ + float hzPerBin = m_sampleRate / (float) m_fftSize; + int bins = channelBandwidth / hzPerBin; + int start = frequencyToBin(centerFrequency) - (bins / 2); + int end = start + bins; + float power = 0.0; + + if (m_linear) + { + float gain = m_useCalibration ? m_calibrationGain : 1.0f; + for (int i = start; i <= end; i++) { + power += m_currentSpectrum[i] * gain; + } + } + else + { + float shift = m_useCalibration ? m_calibrationShiftdB : 0.0f; + for (int i = start; i <= end; i++) { + power += CalcDb::powerFromdB(m_currentSpectrum[i]) + m_calibrationShiftdB; + } + } + + return CalcDb::dbPower(power); +} + void GLSpectrum::stopDrag() { if (m_cursorState != CSNormal) @@ -2645,14 +3103,10 @@ void GLSpectrum::applyChanges() // Peak details in top info line QString minFrequencyStr = displayFull(m_centerFrequency - m_sampleRate/2); // This can be wider if negative, while max is positive QString maxFrequencyStr = displayFull(m_centerFrequency + m_sampleRate/2); - QString widestFrequencyStr = minFrequencyStr.size() > maxFrequencyStr.size() ? minFrequencyStr : maxFrequencyStr; - widestFrequencyStr = widestFrequencyStr.append("Hz"); - m_peakLabelStr = "Peak:"; - m_peakSpaceWidth = fm.width(" "); - m_peakSpaceMidWidth = ((widestFrequencyStr.size() > 10) ? 3 : 1) * m_peakSpaceWidth; // Extra space when lots of digits - m_peakLabelWidth = fm.width(m_peakLabelStr); - m_peakPowerMaxWidth = m_linear ? fm.width("8.000e-10") : fm.width("-100.0dB"); - m_peakFrequencyMaxWidth = fm.width(widestFrequencyStr); + 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"; bool fftSizeChanged = true; @@ -3929,32 +4383,12 @@ int GLSpectrum::getPrecision(int value) } } -// Find power and frequency of max peak in current spectrum -void GLSpectrum::findPeak(float &power, float &frequency) const +void GLSpectrum::drawTextRight(const QString &text, const QString &value, const QString &max, const QString &units) { - int bin; - - bin = 0; - power = m_currentSpectrum[0]; - for (int i = 1; i < m_nbBins; i++) - { - if (m_currentSpectrum[i] > power) - { - power = m_currentSpectrum[i]; - bin = i; - } - } - - power = m_linear ? - power * (m_useCalibration ? m_calibrationGain : 1.0f): - power + (m_useCalibration ? m_calibrationShiftdB : 0.0f); - - float hzPerBin = (float) m_sampleRate / m_fftSize; - frequency = m_centerFrequency + (bin - (m_fftSize/2)) * hzPerBin; + drawTextsRight({text}, {value}, {max}, {units}); } -// Draws peak power and frequency right justified in top info bar -void GLSpectrum::drawPeakText(float power, int64_t frequency, bool units) +void GLSpectrum::drawTextsRight(const QStringList &text, const QStringList &value, const QStringList &max, const QStringList &units) { QFontMetrics fm(font()); @@ -3968,30 +4402,24 @@ void GLSpectrum::drawPeakText(float power, int64_t frequency, bool units) painter.setPen(QColor(0xf0, 0xf0, 0xff)); painter.setFont(font()); - QString powerStr = displayPower( - power, - m_linear ? 'e' : 'f', - m_linear ? 3 : 1 - ); - QString frequencyStr = displayFull(frequency); - if (units) - { - if (!m_linear) { - powerStr = powerStr.append("dB"); - } - frequencyStr = frequencyStr.append("Hz"); - } - - int powerWidth = fm.width(powerStr); - int frequencyWidth = fm.width(frequencyStr); - int x = width() - m_rightMargin; int y = fm.height() + fm.ascent() / 2 - 2; - painter.drawText(QPointF(x - frequencyWidth, y), frequencyStr); - x -= m_peakFrequencyMaxWidth + m_peakSpaceMidWidth; - painter.drawText(QPointF(x - powerWidth, y), powerStr); - x -= m_peakPowerMaxWidth + m_peakSpaceWidth; - painter.drawText(QPointF(x - m_peakLabelWidth, y), m_peakLabelStr); + int textWidth, maxWidth; + for (int i = text.length() - 1; i >= 0; i--) + { + textWidth = fm.width(units[i]); + painter.drawText(QPointF(x - textWidth, y), units[i]); + x -= textWidth; + + textWidth = fm.width(value[i]); + maxWidth = fm.width(max[i]); + painter.drawText(QPointF(x - textWidth, y), value[i]); + x -= maxWidth; + + textWidth = fm.width(text[i]); + painter.drawText(QPointF(x - textWidth, y), text[i]); + x -= textWidth; + } m_glShaderTextOverlay.initTexture(m_infoPixmap.toImage()); @@ -4090,6 +4518,9 @@ void GLSpectrum::formatTextInfo(QString& info) getFrequencyZoom(centerFrequency, frequencySpan); info.append(tr("CF:%1 ").arg(displayScaled(centerFrequency, 'f', getPrecision(centerFrequency/frequencySpan), true))); info.append(tr("SP:%1 ").arg(displayScaled(frequencySpan, 'f', 3, true))); + if (m_measurement != SpectrumSettings::MeasurementNone) { + info.append(tr("RBW:%1 ").arg(displayScaled(m_sampleRate / (float)m_fftSize, 'f', 3, true))); + } } } diff --git a/sdrgui/gui/glspectrum.h b/sdrgui/gui/glspectrum.h index 3e8200f0b..73fc6d0a1 100644 --- a/sdrgui/gui/glspectrum.h +++ b/sdrgui/gui/glspectrum.h @@ -161,6 +161,9 @@ public: void setDisplayTraceIntensity(int intensity); void setLinear(bool linear); void setUseCalibration(bool useCalibration); + void setMeasurementParams(SpectrumSettings::Measurement measurement, int bandwidth, + int chSpacing, int adjChBandwidth, + int harmonics, bool highlight); qint32 getSampleRate() const { return m_sampleRate; } void addChannelMarker(ChannelMarker* channelMarker); @@ -294,12 +297,9 @@ private: QMatrix4x4 m_glLeftScaleBoxMatrix; QMatrix4x4 m_glInfoBoxMatrix; - QString m_peakLabelStr; - int m_peakLabelWidth; - int m_peakSpaceWidth; - int m_peakSpaceMidWidth; - int m_peakPowerMaxWidth; - int m_peakFrequencyMaxWidth; + QString m_peakFrequencyMaxStr; + QString m_peakPowerMaxStr; + QString m_peakPowerUnits; QRgb m_waterfallPalette[240]; QImage* m_waterfallBuffer; @@ -375,6 +375,15 @@ private: QOpenGLDebugLogger *m_openGLLogger; bool m_isDeviceSpectrum; + SpectrumSettings::Measurement m_measurement; + int m_measurementBandwidth; + int m_measurementChSpacing; + int m_measurementAdjChBandwidth; + int m_measurementHarmonics; + bool m_measurementHighlight; + static const QVector4D m_measurementLightMarkerColor; + static const QVector4D m_measurementDarkMarkerColor; + void updateWaterfall(const Real *spectrum); void update3DSpectrogram(const Real *spectrum); void updateHistogram(const Real *spectrum); @@ -382,9 +391,25 @@ private: void initializeGL(); void resizeGL(int width, int height); void paintGL(); + void drawPowerBandMarkers(float max, float min, const QVector4D &color); + void drawBandwidthMarkers(int64_t centerFrequency, int bandwidth, const QVector4D &color); + void drawPeakMarkers(int64_t startFrequency, int64_t endFrequency, const QVector4D &color); void drawSpectrumMarkers(); void drawAnnotationMarkers(); + void measurePeak(); + void measureChannelPower(); + void measureAdjacentChannelPower(); + void measureSNR(); + void measureSFDR(); + float calcChannelPower(int64_t centerFrequency, int channelBandwidth) const; + float calPower(float power) const; + int findPeakBin() const; + void findPeak(float &power, float &frequency) const; + void peakWidth(int center, int &left, int &right, int maxLeft, int maxRight) const; + int GLSpectrum::frequencyToBin(int64_t frequency) const; + int64_t GLSpectrum::binToFrequency(int bin) const; + void stopDrag(); void applyChanges(); @@ -414,8 +439,8 @@ private: static QString displayScaledF(float value, char type, int precision, bool showMult); static QString displayPower(float value, char type, int precision); int getPrecision(int value); - void findPeak(float &power, float &frequency) const; - void drawPeakText(float power, int64_t frequency, bool units=true); + void drawTextRight(const QString &text, const QString &value, const QString &max, const QString &units); + void drawTextsRight(const QStringList &text, const QStringList &value, const QStringList &max, const QStringList &units); void drawTextOverlay( //!< Draws a text overlay const QString& text, const QColor& color, diff --git a/sdrgui/gui/glspectrumgui.cpp b/sdrgui/gui/glspectrumgui.cpp index 9b369d9be..27bdef877 100644 --- a/sdrgui/gui/glspectrumgui.cpp +++ b/sdrgui/gui/glspectrumgui.cpp @@ -49,8 +49,9 @@ GLSpectrumGUI::GLSpectrumGUI(QWidget* parent) : ui->setupUi(this); // Use the custom flow layout for the 3 main horizontal layouts (lines) - ui->verticalLayout->removeItem(ui->Line5Layout); + ui->verticalLayout->removeItem(ui->Line7Layout); ui->verticalLayout->removeItem(ui->Line6Layout); + ui->verticalLayout->removeItem(ui->Line5Layout); ui->verticalLayout->removeItem(ui->Line4Layout); ui->verticalLayout->removeItem(ui->Line3Layout); ui->verticalLayout->removeItem(ui->Line2Layout); @@ -62,9 +63,11 @@ GLSpectrumGUI::GLSpectrumGUI(QWidget* parent) : flowLayout->addItem(ui->Line4Layout); flowLayout->addItem(ui->Line5Layout); flowLayout->addItem(ui->Line6Layout); + flowLayout->addItem(ui->Line7Layout); ui->verticalLayout->addItem(flowLayout); on_linscale_toggled(false); + on_measurement_currentIndexChanged(0); QString levelStyle = QString( "QSpinBox {background-color: rgb(79, 79, 79);}" @@ -224,6 +227,13 @@ void GLSpectrumGUI::displaySettings() ui->calibration->setChecked(m_settings.m_useCalibration); displayGotoMarkers(); + ui->measurement->setCurrentIndex((int) m_settings.m_measurement); + ui->highlight->setChecked(m_settings.m_measurementHighlight); + ui->bandwidth->setValue(m_settings.m_measurementBandwidth); + ui->chSpacing->setValue(m_settings.m_measurementChSpacing); + ui->adjChBandwidth->setValue(m_settings.m_measurementAdjChBandwidth); + ui->harmonics->setValue(m_settings.m_measurementHarmonics); + ui->fftWindow->blockSignals(false); ui->averaging->blockSignals(false); ui->averagingMode->blockSignals(false); @@ -330,6 +340,15 @@ void GLSpectrumGUI::applySpectrumSettings() m_glSpectrum->setMarkersDisplay(m_settings.m_markersDisplay); m_glSpectrum->setCalibrationPoints(m_settings.m_calibrationPoints); m_glSpectrum->setCalibrationInterpMode(m_settings.m_calibrationInterpMode); + + m_glSpectrum->setMeasurementParams( + m_settings.m_measurement, + m_settings.m_measurementBandwidth, + m_settings.m_measurementChSpacing, + m_settings.m_measurementAdjChBandwidth, + m_settings.m_measurementHarmonics, + m_settings.m_measurementHighlight + ); } void GLSpectrumGUI::on_fftWindow_currentIndexChanged(int index) @@ -965,3 +984,59 @@ void GLSpectrumGUI::updateCalibrationPoints() m_glSpectrum->updateCalibrationPoints(); } } + +void GLSpectrumGUI::on_measurement_currentIndexChanged(int index) +{ + m_settings.m_measurement = (SpectrumSettings::Measurement)index; + + bool highlight = (m_settings.m_measurement >= SpectrumSettings::MeasurementChannelPower); + ui->highlight->setVisible(highlight); + + bool bw = (m_settings.m_measurement == SpectrumSettings::MeasurementChannelPower) + || (m_settings.m_measurement == SpectrumSettings::MeasurementAdjacentChannelPower); + ui->bandwidthLabel->setVisible(bw); + ui->bandwidth->setVisible(bw); + + bool adj = m_settings.m_measurement == SpectrumSettings::MeasurementAdjacentChannelPower; + ui->chSpacingLabel->setVisible(adj); + ui->chSpacing->setVisible(adj); + ui->adjChBandwidthLabel->setVisible(adj); + ui->adjChBandwidth->setVisible(adj); + + bool harmonics = (m_settings.m_measurement >= SpectrumSettings::MeasurementSNR) + && (m_settings.m_measurement <= SpectrumSettings::MeasurementSINAD); + ui->harmonicsLabel->setVisible(harmonics); + ui->harmonics->setVisible(harmonics); + + applySettings(); +} + +void GLSpectrumGUI::on_highlight_toggled(bool checked) +{ + m_settings.m_measurementHighlight = checked; + applySettings(); +} + +void GLSpectrumGUI::on_bandwidth_valueChanged(int value) +{ + m_settings.m_measurementBandwidth = value; + applySettings(); +} + +void GLSpectrumGUI::on_chSpacing_valueChanged(int value) +{ + m_settings.m_measurementChSpacing = value; + applySettings(); +} + +void GLSpectrumGUI::on_adjChBandwidth_valueChanged(int value) +{ + m_settings.m_measurementAdjChBandwidth = value; + applySettings(); +} + +void GLSpectrumGUI::on_harmonics_valueChanged(int value) +{ + m_settings.m_measurementHarmonics = value; + applySettings(); +} diff --git a/sdrgui/gui/glspectrumgui.h b/sdrgui/gui/glspectrumgui.h index 4d0ecc627..0a13a981a 100644 --- a/sdrgui/gui/glspectrumgui.h +++ b/sdrgui/gui/glspectrumgui.h @@ -122,6 +122,13 @@ private slots: void on_calibration_toggled(bool checked); void on_gotoMarker_currentIndexChanged(int index); + void on_measurement_currentIndexChanged(int index); + void on_highlight_toggled(bool checked); + void on_bandwidth_valueChanged(int value); + void on_chSpacing_valueChanged(int value); + void on_adjChBandwidth_valueChanged(int value); + void on_harmonics_valueChanged(int value); + void handleInputMessages(); void openWebsocketSpectrumSettingsDialog(const QPoint& p); void openCalibrationPointsDialog(const QPoint& p); diff --git a/sdrgui/gui/glspectrumgui.ui b/sdrgui/gui/glspectrumgui.ui index 5c78eaae9..26d46dcd6 100644 --- a/sdrgui/gui/glspectrumgui.ui +++ b/sdrgui/gui/glspectrumgui.ui @@ -7,7 +7,7 @@ 0 0 630 - 250 + 274 @@ -1118,6 +1118,185 @@ + + + + + + Measurement + + + + None + + + + + Peak + + + + + Ch Power + + + + + Adj Ch + + + + + SNR + + + + + SNFR + + + + + THD + + + + + THD+N + + + + + SINAD + + + + + SFDR + + + + + + + + Highlight measurement + + + Max Hold + + + + :/carrier.png:/carrier.png + + + + 16 + 16 + + + + true + + + + + + + B/W + + + 2 + + + + + + + Measurement bandwidth (Hz) + + + 1 + + + 100000000 + + + 1000 + + + + + + + Spacing + + + 2 + + + + + + + Channel spacing (Hz) + + + 100000000 + + + 1000 + + + + + + + Adj. Ch. B/W + + + 2 + + + + + + + Adjacent channel bandwidth (Hz) + + + 1 + + + 100000000 + + + 1000 + + + + + + + Harmonics + + + 2 + + + + + + + Number of harmonics + + + 20 + + + + + diff --git a/sdrgui/gui/spectrum.md b/sdrgui/gui/spectrum.md index 96c6c9c9b..d92a7c364 100644 --- a/sdrgui/gui/spectrum.md +++ b/sdrgui/gui/spectrum.md @@ -356,6 +356,78 @@ Right click to open the [calibration management dialog](spectrumcalibration.md) This combo only appears if the spectrum display is the spectrum of a device (i.e. main spectrum) and if there are visible annotation markers. It allows to set the device center frequency to the frequency of the selected annotation marker. +

B.7.1: Measurement

+ +Selects a measurement to perform on the spectrum: + +* None - No measurement is performed. +* Peak - Displays highest peak power and frequency. +* Ch Power - Channel power. +* Adj Ch - Adjacent channel power. +* SNR - Signal to Noise Ratio. +* SNFR - Signal to Noise Floor Ratio. +* THD - Total Harmonic Distortion. +* THD+N - Total Harmonic Distortion plus Noise. +* SINAD - Signal to Noise and Distortion ratio. +* SFDR - Spurious Free Dynamic Range + +The measurement result is displayed in the top-right of the spectrum. +When any measurement is selected, the resolution bandwidth (RBW), which is the sample rate / FFT size, is additionally displayed in the top-left of the spectrum. +Several of the measurements highlight the measurement region on the spectrum. This can be toggled on and off with the 'Highlight measurement' button. + +
Peak
+ +The peak measurement displays the power and frequency of the FFT bin with the highest magnitude. + +![Peak measurement](../../doc/img/Specturm_Measurement_Peak.png) + +
Channel Power
+ +Channel power measures the total power within a user-defined bandwidth, at the center of the spectrum: + +![Adjacent channel power measurement](../../doc/img/Specturm_Measurement_ChannelPower.png) + +
Adjacent Channel Power
+ +The adjacent channel power measurement measures the power in a channel of user-defined bandwidth at the center of the spectrum and compares it to the power in the left and right adjacent channels. +Channel separation is specifed in the 'Spacing' field. + +![Adjacent channel power measurement](../../doc/img/Specturm_Measurement_AdjChannelPower.png) + +
Signal to Noise Ratio
+ +The SNR measurement estimates a signal-to-noise ratio. +The fundamental signal is the largest peak (i.e. FFT bin with highest magnitude). +The bandwidth of the signal is assumed to be the width of the largest peak, which includes adjacent bins with a monotonically decreasing magnitude. +Noise is summed over the full bandwidth (i.e all FFT bins), with the fundamental and user-specified number of harmonics being replaced with the noise median from outside of these regions. +The noise median is also subtracted from the signal, before the SNR is calculated. + +![SNR measurement](../../doc/img/Specturm_Measurement_SNR.png) + +
Signal to Noise Floor Ratio
+ +The SNFR measurement estimates a signal-to-noise-floor ratio. +This is similar to the SNR, except that the noise used in the ratio, is only the median noise value calculated from the noise outside of the fundamental and harmonics, summed over the bandwidth of the signal. +One way to think of this, is that it is the SNR if all noise outside of the signal's bandwidth was filtered. + +
Total Harmonic Distortion
+ +THD is measured as per SNR, but the result is the ratio of the total power of the harmonics to the fundamental. + +
Total Harmonic Distortion Plus Noise
+ +THD+N is measured as per SNR, but the result is the ratio of the total power of the harmonics and noise to the fundamental. + +
Signal to Noise and Distortion Ratio
+ +SINAD is measured as per SNR, but the result is the ratio of the fundamental to the total power of the harmonics and noise. + +
Spurious Free Dynamic Range
+ +SFDR is a measurement of the difference in power from the largest peak (the fundamental) to the second largest peak (the strongest spurious signal). + +![SFDR measurement](../../doc/img/Specturm_Measurement_SFDR.png) +

3D Spectrogram Controls

![3D Spectrogram](../../doc/img/MainWindow_3D_spectrogram.png)