diff --git a/plugins/channelrx/freqscanner/freqscanner.cpp b/plugins/channelrx/freqscanner/freqscanner.cpp index 0228d18f8..bdabfcf0c 100644 --- a/plugins/channelrx/freqscanner/freqscanner.cpp +++ b/plugins/channelrx/freqscanner/freqscanner.cpp @@ -454,9 +454,11 @@ void FreqScanner::processScanResults(const QDateTime& fftStartTime, const QList< { frequencySettings = m_settings.getFrequencySettings(m_scanResults[i].m_frequency); Real threshold = m_settings.getThreshold(frequencySettings); + if (m_scanResults[i].m_power >= threshold) { - if (!activeFrequencySettings || (m_scanResults[i].m_power > maxPower)) + if (!activeFrequencySettings || ((m_scanResults[i].m_power > maxPower) + && checkVoiceThreshold(m_settings.m_voiceSquelchType, m_scanResults[i].m_voiceActivityLevel, m_settings.m_voiceSquelchThreshold))) { frequency = m_scanResults[i].m_frequency; maxPower = m_scanResults[i].m_power; @@ -465,14 +467,16 @@ void FreqScanner::processScanResults(const QDateTime& fftStartTime, const QList< } } } - else + else // TABLE_ORDER { // Find first frequency in list above threshold for (int i = 0; i < m_scanResults.size(); i++) { frequencySettings = m_settings.getFrequencySettings(m_scanResults[i].m_frequency); Real threshold = m_settings.getThreshold(frequencySettings); - if (m_scanResults[i].m_power >= threshold) + + if ((m_scanResults[i].m_power >= threshold) + && checkVoiceThreshold(m_settings.m_voiceSquelchType, m_scanResults[i].m_voiceActivityLevel, m_settings.m_voiceSquelchThreshold)) { frequency = m_scanResults[i].m_frequency; activeFrequencySettings = frequencySettings; @@ -787,6 +791,11 @@ void FreqScanner::unmuteAll() m_autoMutedChannels.clear(); } +bool FreqScanner::checkVoiceThreshold(FreqScannerSettings::VoiceSquelchType voiceSquelchType, Real voiceActivityLevel, Real voiceSquelchThreshold) +{ + return (voiceSquelchType == FreqScannerSettings::VoiceSquelchType::None) || (voiceActivityLevel >= voiceSquelchThreshold); +} + void FreqScanner::applyChannelSetting(const QString& channel) { if (!MainCore::getDeviceAndChannelIndexFromId(channel, m_scanDeviceSetIndex, m_scanChannelIndex)) { diff --git a/plugins/channelrx/freqscanner/freqscanner.h b/plugins/channelrx/freqscanner/freqscanner.h index 6cea330d6..d3b3b63ce 100644 --- a/plugins/channelrx/freqscanner/freqscanner.h +++ b/plugins/channelrx/freqscanner/freqscanner.h @@ -154,9 +154,10 @@ public: struct ScanResult { qint64 m_frequency; float m_power; + float m_voiceActivityLevel; // 0.0-1.0, voice likelihood for SSB modes }; - const QDateTime& getFFTStartTime() { return m_fftStartTime; } + const QDateTime& getFFTStartTime() const { return m_fftStartTime; } QList& getScanResults() { return m_scanResults; } static MsgScanResult* create(const QDateTime& fftStartTime) { @@ -428,6 +429,7 @@ private: void unmuteAll(); void mute(unsigned int deviceSetIndex, unsigned int channelIndex); void unmute(unsigned int deviceSetIndex, unsigned int channelIndex); + bool checkVoiceThreshold(FreqScannerSettings::VoiceSquelchType voiceSquelchType, Real voiceActivityLevel, Real voiceSquelchThreshold); static QList *createFrequencyList(const FreqScannerSettings& settings); @@ -440,4 +442,3 @@ private slots: }; #endif // INCLUDE_FREQSCANNER_H - diff --git a/plugins/channelrx/freqscanner/freqscannergui.cpp b/plugins/channelrx/freqscanner/freqscannergui.cpp index cf9b2e616..723947cbe 100644 --- a/plugins/channelrx/freqscanner/freqscannergui.cpp +++ b/plugins/channelrx/freqscanner/freqscannergui.cpp @@ -361,6 +361,19 @@ void FreqScannerGUI::on_thresh_valueChanged(int value) applySetting("threshold"); } +void FreqScannerGUI::on_voiceThreshold_valueChanged(int value) +{ + ui->voiceThresholdText->setText(QString("%1").arg(value / 100.0, 0, 'f', 2)); + m_settings.m_voiceSquelchThreshold = value / 100.0; + applySetting("voiceSquelchThreshold"); +} + +void FreqScannerGUI::on_voiceSquelchType_currentIndexChanged(int index) +{ + m_settings.m_voiceSquelchType = (FreqScannerSettings::VoiceSquelchType)index; + applySetting("voiceSquelchType"); +} + void FreqScannerGUI::on_priority_currentIndexChanged(int index) { m_settings.m_priority = (FreqScannerSettings::Priority)index; @@ -607,6 +620,9 @@ void FreqScannerGUI::displaySettings() ui->tuneTimeText->setText(QString("%1 ms").arg(m_settings.m_tuneTime)); ui->thresh->setValue(m_settings.m_threshold * 10.0); ui->threshText->setText(QString("%1 dB").arg(m_settings.m_threshold, 0, 'f', 1)); + ui->voiceThreshold->setValue(m_settings.m_voiceSquelchThreshold * 100.0); + ui->voiceThresholdText->setText(QString("%1").arg(m_settings.m_voiceSquelchThreshold, 0, 'f', 2)); + ui->voiceSquelch->setCurrentIndex((int)m_settings.m_voiceSquelchType); ui->priority->setCurrentIndex((int)m_settings.m_priority); ui->measurement->setCurrentIndex((int)m_settings.m_measurement); ui->mode->setCurrentIndex((int)m_settings.m_mode); @@ -1256,6 +1272,8 @@ void FreqScannerGUI::makeUIConnections() QObject::connect(ui->retransmitTime, &QDial::valueChanged, this, &FreqScannerGUI::on_retransmitTime_valueChanged); QObject::connect(ui->tuneTime, &QDial::valueChanged, this, &FreqScannerGUI::on_tuneTime_valueChanged); QObject::connect(ui->thresh, &QDial::valueChanged, this, &FreqScannerGUI::on_thresh_valueChanged); + QObject::connect(ui->voiceSquelch, QOverload::of(&QComboBox::currentIndexChanged), this, &FreqScannerGUI::on_voiceSquelchType_currentIndexChanged); + QObject::connect(ui->voiceThreshold, &QDial::valueChanged, this, &FreqScannerGUI::on_voiceThreshold_valueChanged); QObject::connect(ui->priority, QOverload::of(&QComboBox::currentIndexChanged), this, &FreqScannerGUI::on_priority_currentIndexChanged); QObject::connect(ui->measurement, QOverload::of(&QComboBox::currentIndexChanged), this, &FreqScannerGUI::on_measurement_currentIndexChanged); QObject::connect(ui->mode, QOverload::of(&QComboBox::currentIndexChanged), this, &FreqScannerGUI::on_mode_currentIndexChanged); diff --git a/plugins/channelrx/freqscanner/freqscannergui.h b/plugins/channelrx/freqscanner/freqscannergui.h index 6cbc08553..4962b7e14 100644 --- a/plugins/channelrx/freqscanner/freqscannergui.h +++ b/plugins/channelrx/freqscanner/freqscannergui.h @@ -132,6 +132,8 @@ private slots: void on_retransmitTime_valueChanged(int value); void on_tuneTime_valueChanged(int value); void on_thresh_valueChanged(int value); + void on_voiceThreshold_valueChanged(int value); + void on_voiceSquelchType_currentIndexChanged(int index); void on_priority_currentIndexChanged(int index); void on_measurement_currentIndexChanged(int index); void on_mode_currentIndexChanged(int index); diff --git a/plugins/channelrx/freqscanner/freqscannergui.ui b/plugins/channelrx/freqscanner/freqscannergui.ui index bb057abaa..94f9bad24 100644 --- a/plugins/channelrx/freqscanner/freqscannergui.ui +++ b/plugins/channelrx/freqscanner/freqscannergui.ui @@ -6,8 +6,8 @@ 0 0 - 516 - 423 + 676 + 410 @@ -39,7 +39,7 @@ 0 0 - 511 + 671 411 @@ -166,6 +166,9 @@ + + Qt::Vertical + 40 @@ -257,8 +260,50 @@ + + + + VTH + + + + + + + + 24 + 24 + + + + Power threshold in dB + + + 0 + + + 100 + + + 1 + + + 50 + + + + + + + 0.01 + + + + + Qt::Vertical + 40 @@ -526,6 +571,9 @@ + + Qt::Vertical + 40 @@ -663,6 +711,32 @@ + + + + VSq + + + + + + + + None + + + + + LSB + + + + + USB + + + + @@ -863,6 +937,9 @@ Leave blank for no adjustment + + Qt::Vertical + 40 @@ -898,18 +975,18 @@ Leave blank for no adjustment QToolButton
gui/buttonswitch.h
- - ValueDialZ - QWidget -
gui/valuedialz.h
- 1 -
RollupContents QWidget
gui/rollupcontents.h
1
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
deltaFrequency diff --git a/plugins/channelrx/freqscanner/freqscannersettings.cpp b/plugins/channelrx/freqscanner/freqscannersettings.cpp index ea7430cba..365d52874 100644 --- a/plugins/channelrx/freqscanner/freqscannersettings.cpp +++ b/plugins/channelrx/freqscanner/freqscannersettings.cpp @@ -46,6 +46,8 @@ void FreqScannerSettings::resetToDefaults() m_scanTime = 0.1f; m_retransmitTime = 2.0f; m_tuneTime = 100; + m_voiceSquelchThreshold = 0.5f; + m_voiceSquelchType = None; m_priority = MAX_POWER; m_measurement = PEAK; m_mode = CONTINUOUS; @@ -76,6 +78,8 @@ QByteArray FreqScannerSettings::serialize() const s.writeS32(2, m_channelBandwidth); s.writeS32(3, m_channelFrequencyOffset); s.writeFloat(4, m_threshold); + s.writeS32(5, (int)m_voiceSquelchType); + s.writeFloat(6, m_voiceSquelchThreshold); s.writeString(8, m_channel); s.writeFloat(9, m_scanTime); s.writeFloat(10, m_retransmitTime); @@ -130,6 +134,8 @@ bool FreqScannerSettings::deserialize(const QByteArray& data) d.readS32(2, &m_channelBandwidth, 25000); d.readS32(3, &m_channelFrequencyOffset, 25000); d.readFloat(4, &m_threshold, -60.0f); + d.readS32(5, (int*)&m_voiceSquelchType, (int)None); + d.readFloat(6, &m_voiceSquelchThreshold, 0.5f); d.readString(8, &m_channel); d.readFloat(9, &m_scanTime, 0.1f); d.readFloat(10, &m_retransmitTime, 2.0f); @@ -222,6 +228,12 @@ void FreqScannerSettings::applySettings(const QStringList& settingsKeys, const F if (settingsKeys.contains("threshold")) { m_threshold = settings.m_threshold; } + if (settingsKeys.contains("voiceSquelchThreshold")) { + m_voiceSquelchThreshold = settings.m_voiceSquelchThreshold; + } + if (settingsKeys.contains("voiceSquelchType")) { + m_voiceSquelchType = settings.m_voiceSquelchType; + } if (settingsKeys.contains("frequencySettings")) { m_frequencySettings = settings.m_frequencySettings; } @@ -303,6 +315,12 @@ QString FreqScannerSettings::getDebugString(const QStringList& settingsKeys, boo if (settingsKeys.contains("threshold") || force) { ostr << " m_threshold: " << m_threshold; } + if (settingsKeys.contains("voiceSquelchThreshold") || force) { + ostr << " m_voiceSquelchThreshold: " << m_voiceSquelchThreshold; + } + if (settingsKeys.contains("voiceSquelchType") || force) { + ostr << " m_voiceSquelchType: " << m_voiceSquelchType; + } if (settingsKeys.contains("frequencySettings") || force) { QStringList s; diff --git a/plugins/channelrx/freqscanner/freqscannersettings.h b/plugins/channelrx/freqscanner/freqscannersettings.h index 9a04b7103..7bfbec10d 100644 --- a/plugins/channelrx/freqscanner/freqscannersettings.h +++ b/plugins/channelrx/freqscanner/freqscannersettings.h @@ -49,11 +49,17 @@ struct FreqScannerSettings qint32 m_channelFrequencyOffset;//!< Minimum DC offset of tuned channel qint32 m_channelShift; //!< Channel frequency shift Real m_threshold; //!< Power threshold in dB + Real m_voiceSquelchThreshold; //!< Voice squelch threshold in the range [0.0, 1.0]. Only relevant if voice squelch is enabled. QString m_channel; //!< Channel (E.g: R1:4) to tune to active frequency QList m_frequencySettings; //!< Frequencies to scan and corresponding settings float m_scanTime; //!< In seconds float m_retransmitTime; //!< In seconds int m_tuneTime; //!< In milliseconds + enum VoiceSquelchType { + None, + VoiceLsb, + VoiceUsb, + } m_voiceSquelchType; //!< Voice squelch type for SSB modes. None means no voice squelch, VoiceLsb means voice squelch on lower sideband frequencies, VoiceUsb means voice squelch on upper sideband frequencies. enum Priority { MAX_POWER, TABLE_ORDER diff --git a/plugins/channelrx/freqscanner/freqscannersink.cpp b/plugins/channelrx/freqscanner/freqscannersink.cpp index c0bb5cd27..446866594 100644 --- a/plugins/channelrx/freqscanner/freqscannersink.cpp +++ b/plugins/channelrx/freqscanner/freqscannersink.cpp @@ -144,8 +144,17 @@ void FreqScannerSink::processOneSample(Complex &ci) } else { power = totalPower(bin, channelBins); } - //qDebug() << "startFrequency:" << startFrequency << "m_scannerSampleRate:" << m_scannerSampleRate << "m_centerFrequency:" << m_centerFrequency << "frequency" << frequency << "bin" << bin << "power" << power; - FreqScanner::MsgScanResult::ScanResult result = {frequency, power}; + + // Calculate voice activity level if using voice trigger + Real voiceLevel = 0.0; + if (m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceLsb) { + voiceLevel = voiceActivityLevel(bin, channelBins, true); + } else if (m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceUsb) { + voiceLevel = voiceActivityLevel(bin, channelBins, false); + } + + //qDebug() << "startFrequency:" << startFrequency << "m_scannerSampleRate:" << m_scannerSampleRate << "m_centerFrequency:" << m_centerFrequency << "frequency" << frequency << "bin" << bin << "power" << power << "voiceLevel" << voiceLevel; + FreqScanner::MsgScanResult::ScanResult result = {frequency, power, voiceLevel}; results.append(result); } } @@ -268,3 +277,152 @@ void FreqScannerSink::applySettings(const FreqScannerSettings& settings, const Q m_settings.applySettings(settingsKeys, settings); } } + +// Voice activity detection for SSB signals +// Detects voice by looking for formant-like structure (broad spectral peaks) +// Returns a value from 0.0 (no voice) to 1.0 (strong voice signature) +Real FreqScannerSink::voiceActivityLevel(int bin, int channelBins, bool isLSB) const +{ + // Voice band in SSB is typically 100-3000 Hz from carrier + // We look for 2-4 formant peaks with bandwidth 50-200 Hz each + + int startBin = bin - channelBins / 2 + 1; + int endBin = startBin + channelBins - 1; + + if (startBin < 0 || endBin >= m_fftSize) { + return 0.0; + } + + // Calculate bin bandwidth in Hz + float binBW = m_scannerSampleRate / (float)m_fftSize; + + // For LSB, spectrum is reversed - flip the search direction + int step = isLSB ? -1 : 1; + int searchStart = isLSB ? endBin : startBin; + int searchEnd = isLSB ? startBin : endBin; + + // Find peaks above noise floor + QVector peakBins; + QVector peakMags; + + // Calculate average noise floor + Real noiseFloor = 0.0; + int noiseCount = 0; + for (int i = startBin; i <= endBin; i++) { + noiseFloor += m_magSq[i]; + noiseCount++; + } + noiseFloor = (noiseCount > 0) ? (noiseFloor / noiseCount) : 1e-12; + Real threshold = noiseFloor * 3.0; // 4.77 dB above noise + + // Simple peak detection + int i = searchStart; + while ((isLSB && i >= searchEnd) || (!isLSB && i <= searchEnd)) + { + if (m_magSq[i] > threshold) + { + // Found potential peak start + Real peakMag = m_magSq[i]; + int peakBin = i; + + // Find local maximum + i += step; + while ((isLSB && i >= searchEnd) || (!isLSB && i <= searchEnd)) + { + if (m_magSq[i] > peakMag) { + peakMag = m_magSq[i]; + peakBin = i; + i += step; + } else if (m_magSq[i] > threshold) { + i += step; + } else { + break; // Peak ended + } + } + + peakBins.append(peakBin); + peakMags.append(peakMag); + } + i += step; + } + + if (peakBins.size() < 2) { + return 0.0; // Need at least 2 peaks for voice + } + + // Measure peak bandwidths + int broadPeakCount = 0; + for (int p = 0; p < peakBins.size(); p++) + { + int peakBin = peakBins[p]; + Real peakMag = peakMags[p]; + Real halfPower = peakMag * 0.5; // 3dB point + + // Measure bandwidth at half power (-3dB) + int bwCount = 1; // Peak bin itself + + // Search left + for (int j = peakBin - 1; j >= startBin; j--) { + if (m_magSq[j] > halfPower) { + bwCount++; + } else { + break; + } + } + + // Search right + for (int j = peakBin + 1; j <= endBin; j++) { + if (m_magSq[j] > halfPower) { + bwCount++; + } else { + break; + } + } + + float bandwidth = bwCount * binBW; + + // Voice formants are typically 50-200 Hz wide + // CW signals are <50 Hz wide + if (bandwidth >= 50.0 && bandwidth <= 200.0) { + broadPeakCount++; + } + } + + // Check formant spacing (voice formants are typically 500-1500 Hz apart) + bool goodSpacing = false; + if (broadPeakCount >= 2 && peakBins.size() >= 2) + { + for (int p = 0; p < peakBins.size() - 1; p++) + { + int spacing = std::abs(peakBins[p + 1] - peakBins[p]); + float spacingHz = spacing * binBW; + if (spacingHz >= 400.0 && spacingHz <= 1800.0) { + goodSpacing = true; + break; + } + } + } + + // Calculate voice activity score + // 2-4 broad peaks with good spacing = strong voice signature + float score = 0.0; + + if (broadPeakCount >= 2) + { + // Base score from number of broad peaks + score = std::min(broadPeakCount / 4.0f, 1.0f); + + // Boost if spacing is good + if (goodSpacing) { + score = std::min(score * 1.5f, 1.0f); + } + + // Penalize if too many narrow peaks (likely CW or noise) + int narrowPeakCount = peakBins.size() - broadPeakCount; + if (narrowPeakCount > broadPeakCount) { + score *= 0.5; + } + } + + return score; +} diff --git a/plugins/channelrx/freqscanner/freqscannersink.h b/plugins/channelrx/freqscanner/freqscannersink.h index 4fe85aeb4..976a312bc 100644 --- a/plugins/channelrx/freqscanner/freqscannersink.h +++ b/plugins/channelrx/freqscanner/freqscannersink.h @@ -80,6 +80,7 @@ private: Real totalPower(int bin, int channelBins) const; Real peakPower(int bin, int channelBins) const; Real magSq(int bin) const; + Real voiceActivityLevel(int bin, int channelBins, bool isLSB) const; }; #endif // INCLUDE_FREQSCANNERSINK_H