1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-22 17:45:48 -05:00

Spectrum: Add Channel Power and SNR measurements

This commit is contained in:
Jon Beniston 2022-09-25 10:50:56 +01:00
parent 36ec0f354d
commit d67ba75a94
13 changed files with 898 additions and 70 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -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);

View File

@ -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

View File

@ -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<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 (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<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 = 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<float>::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)));
}
}
}

View File

@ -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,

View File

@ -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();
}

View File

@ -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);

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>630</width>
<height>250</height>
<height>274</height>
</rect>
</property>
<property name="font">
@ -1118,6 +1118,185 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="Line7Layout">
<item>
<widget class="QComboBox" name="measurement">
<property name="toolTip">
<string>Measurement</string>
</property>
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Peak</string>
</property>
</item>
<item>
<property name="text">
<string>Ch Power</string>
</property>
</item>
<item>
<property name="text">
<string>Adj Ch</string>
</property>
</item>
<item>
<property name="text">
<string>SNR</string>
</property>
</item>
<item>
<property name="text">
<string>SNFR</string>
</property>
</item>
<item>
<property name="text">
<string>THD</string>
</property>
</item>
<item>
<property name="text">
<string>THD+N</string>
</property>
</item>
<item>
<property name="text">
<string>SINAD</string>
</property>
</item>
<item>
<property name="text">
<string>SFDR</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="highlight">
<property name="toolTip">
<string>Highlight measurement</string>
</property>
<property name="text">
<string>Max Hold</string>
</property>
<property name="icon">
<iconset resource="../resources/res.qrc">
<normaloff>:/carrier.png</normaloff>:/carrier.png</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="bandwidthLabel">
<property name="text">
<string>B/W</string>
</property>
<property name="margin">
<number>2</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="bandwidth">
<property name="toolTip">
<string>Measurement bandwidth (Hz)</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100000000</number>
</property>
<property name="singleStep">
<number>1000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="chSpacingLabel">
<property name="text">
<string>Spacing</string>
</property>
<property name="margin">
<number>2</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="chSpacing">
<property name="toolTip">
<string>Channel spacing (Hz)</string>
</property>
<property name="maximum">
<number>100000000</number>
</property>
<property name="singleStep">
<number>1000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="adjChBandwidthLabel">
<property name="text">
<string>Adj. Ch. B/W</string>
</property>
<property name="margin">
<number>2</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="adjChBandwidth">
<property name="toolTip">
<string>Adjacent channel bandwidth (Hz)</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100000000</number>
</property>
<property name="singleStep">
<number>1000</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="harmonicsLabel">
<property name="text">
<string>Harmonics</string>
</property>
<property name="margin">
<number>2</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="harmonics">
<property name="toolTip">
<string>Number of harmonics</string>
</property>
<property name="maximum">
<number>20</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>

View File

@ -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.
<h4>B.7.1: Measurement</h4>
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.
<h5>Peak</h5>
The peak measurement displays the power and frequency of the FFT bin with the highest magnitude.
![Peak measurement](../../doc/img/Specturm_Measurement_Peak.png)
<h5>Channel Power</h5>
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)
<h5>Adjacent Channel Power</h5>
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)
<h5>Signal to Noise Ratio</h5>
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)
<h5>Signal to Noise Floor Ratio</h5>
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.
<h5>Total Harmonic Distortion</h5>
THD is measured as per SNR, but the result is the ratio of the total power of the harmonics to the fundamental.
<h5>Total Harmonic Distortion Plus Noise</h5>
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.
<h5>Signal to Noise and Distortion Ratio</h5>
SINAD is measured as per SNR, but the result is the ratio of the fundamental to the total power of the harmonics and noise.
<h5>Spurious Free Dynamic Range</h5>
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)
<h2>3D Spectrogram Controls</h2>
![3D Spectrogram](../../doc/img/MainWindow_3D_spectrogram.png)