mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-08-26 13:26:24 -04:00
Merge pull request #2651 from f4exb/feature-fscan-voicesquelch
Voice Activity detection for Frequency Scanner in SSB modes
This commit is contained in:
@@ -20,6 +20,7 @@ These instructions guide Copilot to generate code that aligns with modern Qt C++
|
||||
- Use **auto** for type deduction when it improves readability
|
||||
- Utilize **smart pointers** (`std::unique_ptr`, `std::shared_ptr`) over raw pointers
|
||||
- Apply **range-based for loops** instead of traditional iterator loops
|
||||
- When the loop index is only used to access container elements, prefer **range-based loops** to avoid index/type mismatch and off-by-one errors (common Sonar findings)
|
||||
- Use **constexpr** for compile-time constants and functions
|
||||
- Leverage **structured bindings** for tuple/pair unpacking
|
||||
- Use **std::optional** for optional values instead of null pointers
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 238 KiB |
Binary file not shown.
@@ -49,6 +49,7 @@ MESSAGE_CLASS_DEFINITION(FreqScanner::MsgReportChannels, Message)
|
||||
MESSAGE_CLASS_DEFINITION(FreqScanner::MsgStartScan, Message)
|
||||
MESSAGE_CLASS_DEFINITION(FreqScanner::MsgStopScan, Message)
|
||||
MESSAGE_CLASS_DEFINITION(FreqScanner::MsgScanComplete, Message)
|
||||
MESSAGE_CLASS_DEFINITION(FreqScanner::MsgContinueScan, Message)
|
||||
MESSAGE_CLASS_DEFINITION(FreqScanner::MsgScanResult, Message)
|
||||
MESSAGE_CLASS_DEFINITION(FreqScanner::MsgStatus, Message)
|
||||
MESSAGE_CLASS_DEFINITION(FreqScanner::MsgReportActiveFrequency, Message)
|
||||
@@ -261,6 +262,12 @@ bool FreqScanner::handleMessage(const Message& cmd)
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (MsgContinueScan::match(cmd))
|
||||
{
|
||||
continueScan();
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
@@ -307,7 +314,7 @@ void FreqScanner::initScan()
|
||||
// }
|
||||
mute(m_scanDeviceSetIndex, m_scanChannelIndex);
|
||||
|
||||
if (m_centerFrequency != m_stepStartFrequency) {
|
||||
if (!m_settings.m_lockDeviceFrequency && (m_centerFrequency != m_stepStartFrequency)) {
|
||||
setDeviceCenterFrequency(m_stepStartFrequency);
|
||||
}
|
||||
|
||||
@@ -320,6 +327,11 @@ void FreqScanner::initScan()
|
||||
m_state = SCAN_FOR_MAX_POWER;
|
||||
}
|
||||
|
||||
void FreqScanner::continueScan()
|
||||
{
|
||||
m_state = SCAN_FOR_MAX_POWER;
|
||||
}
|
||||
|
||||
void FreqScanner::processScanResults(const QDateTime& fftStartTime, const QList<MsgScanResult::ScanResult>& results)
|
||||
{
|
||||
switch (m_state)
|
||||
@@ -374,54 +386,77 @@ void FreqScanner::processScanResults(const QDateTime& fftStartTime, const QList<
|
||||
}
|
||||
|
||||
// Calculate next center frequency
|
||||
bool complete = false; // Have all frequencies been scanned?
|
||||
bool freqInRange = false;
|
||||
const bool lockDeviceFrequency = m_settings.m_lockDeviceFrequency;
|
||||
const qint64 currentCenterFrequency = m_centerFrequency;
|
||||
bool complete = lockDeviceFrequency; // Have all frequencies been scanned?
|
||||
bool freqInRange = lockDeviceFrequency;
|
||||
qint64 nextCenterFrequency = m_centerFrequency;
|
||||
int usableBW = (m_scannerSampleRate * 3 / 4) & ~1;
|
||||
int nextFrequencyIndex = 0;
|
||||
do
|
||||
int nextFrequencyIndex = -1;
|
||||
const auto isInCurrentScanRange = [currentCenterFrequency, usableBW](qint64 frequency)
|
||||
{
|
||||
if (nextCenterFrequency + usableBW / 2 > m_stepStopFrequency)
|
||||
{
|
||||
nextCenterFrequency = m_stepStartFrequency;
|
||||
complete = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextCenterFrequency += usableBW;
|
||||
complete = false;
|
||||
}
|
||||
return (frequency >= currentCenterFrequency - usableBW / 2)
|
||||
&& (frequency < currentCenterFrequency + usableBW / 2);
|
||||
};
|
||||
|
||||
// Are any frequencies in this new range?
|
||||
if (!lockDeviceFrequency)
|
||||
{
|
||||
do
|
||||
{
|
||||
if (nextCenterFrequency + usableBW / 2 > m_stepStopFrequency)
|
||||
{
|
||||
nextCenterFrequency = m_stepStartFrequency;
|
||||
complete = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextCenterFrequency += usableBW;
|
||||
complete = false;
|
||||
}
|
||||
|
||||
// Are any frequencies in this new range?
|
||||
for (int i = 0; i < m_settings.m_frequencySettings.size(); i++)
|
||||
{
|
||||
if (m_settings.m_frequencySettings[i].m_enabled
|
||||
&& (m_settings.m_frequencySettings[i].m_frequency >= nextCenterFrequency - usableBW / 2)
|
||||
&& (m_settings.m_frequencySettings[i].m_frequency < nextCenterFrequency + usableBW / 2))
|
||||
{
|
||||
freqInRange = true;
|
||||
nextFrequencyIndex = i;
|
||||
|
||||
// Do we need to realign for frequencies with wider bandwidths than default
|
||||
if (!m_settings.m_frequencySettings[i].m_channelBandwidth.isEmpty())
|
||||
{
|
||||
bool ok;
|
||||
int channelBW = m_settings.m_frequencySettings[i].m_channelBandwidth.toInt(&ok);
|
||||
if (ok)
|
||||
{
|
||||
if (channelBW >= usableBW) {
|
||||
nextCenterFrequency = m_settings.m_frequencySettings[i].m_frequency;
|
||||
} else if (m_settings.m_frequencySettings[i].m_frequency - channelBW / 2 < nextCenterFrequency - usableBW / 2) {
|
||||
nextCenterFrequency = m_settings.m_frequencySettings[i].m_frequency - channelBW / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!complete && !freqInRange);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < m_settings.m_frequencySettings.size(); i++)
|
||||
{
|
||||
if (m_settings.m_frequencySettings[i].m_enabled
|
||||
&& (m_settings.m_frequencySettings[i].m_frequency >= nextCenterFrequency - usableBW / 2)
|
||||
&& (m_settings.m_frequencySettings[i].m_frequency < nextCenterFrequency + usableBW / 2))
|
||||
&& isInCurrentScanRange(m_settings.m_frequencySettings[i].m_frequency))
|
||||
{
|
||||
freqInRange = true;
|
||||
nextFrequencyIndex = i;
|
||||
|
||||
// Do we need to realign for frequencies with wider bandwidths than default
|
||||
if (!m_settings.m_frequencySettings[i].m_channelBandwidth.isEmpty())
|
||||
{
|
||||
bool ok;
|
||||
int channelBW = m_settings.m_frequencySettings[i].m_channelBandwidth.toInt(&ok);
|
||||
if (ok)
|
||||
{
|
||||
if (channelBW >= usableBW) {
|
||||
nextCenterFrequency = m_settings.m_frequencySettings[i].m_frequency;
|
||||
} else if (m_settings.m_frequencySettings[i].m_frequency - channelBW / 2 < nextCenterFrequency - usableBW / 2) {
|
||||
nextCenterFrequency = m_settings.m_frequencySettings[i].m_frequency - channelBW / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!complete && !freqInRange);
|
||||
|
||||
if (complete || (m_settings.m_mode == FreqScannerSettings::MULTIPLEX))
|
||||
{
|
||||
@@ -442,39 +477,67 @@ void FreqScanner::processScanResults(const QDateTime& fftStartTime, const QList<
|
||||
|
||||
if (m_settings.m_mode == FreqScannerSettings::MULTIPLEX)
|
||||
{
|
||||
activeFrequencySettings = &m_settings.m_frequencySettings[nextFrequencyIndex];
|
||||
frequency = activeFrequencySettings->m_frequency;
|
||||
if (nextFrequencyIndex >= 0)
|
||||
{
|
||||
activeFrequencySettings = &m_settings.m_frequencySettings[nextFrequencyIndex];
|
||||
frequency = activeFrequencySettings->m_frequency;
|
||||
}
|
||||
}
|
||||
else if (m_settings.m_priority == FreqScannerSettings::MAX_POWER)
|
||||
{
|
||||
Real maxPower = -200.0f;
|
||||
Real maxVoiceActivityLevel = 0.0f;
|
||||
|
||||
// Find frequency with max power that exceeds thresholds
|
||||
for (int i = 0; i < m_scanResults.size(); i++)
|
||||
{
|
||||
if (lockDeviceFrequency && !isInCurrentScanRange(m_scanResults[i].m_frequency)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
frequencySettings = m_settings.getFrequencySettings(m_scanResults[i].m_frequency);
|
||||
Real threshold = m_settings.getThreshold(frequencySettings);
|
||||
if (m_scanResults[i].m_power >= threshold)
|
||||
|
||||
if (m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceSquelchType::None)
|
||||
{
|
||||
if (!activeFrequencySettings || (m_scanResults[i].m_power > maxPower))
|
||||
if ((m_scanResults[i].m_power >= threshold)
|
||||
&& (!activeFrequencySettings || (m_scanResults[i].m_power > maxPower)))
|
||||
{
|
||||
frequency = m_scanResults[i].m_frequency;
|
||||
maxPower = m_scanResults[i].m_power;
|
||||
activeFrequencySettings = frequencySettings;
|
||||
}
|
||||
}
|
||||
else // VAD
|
||||
{
|
||||
if ((m_scanResults[i].m_voiceActivityLevel >= m_settings.m_voiceSquelchThreshold)
|
||||
&& (!activeFrequencySettings || (m_scanResults[i].m_voiceActivityLevel > maxVoiceActivityLevel)))
|
||||
{
|
||||
frequency = m_scanResults[i].m_frequency;
|
||||
maxVoiceActivityLevel = m_scanResults[i].m_voiceActivityLevel;
|
||||
activeFrequencySettings = frequencySettings;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
int j = m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceSquelchType::VoiceLsb ?
|
||||
m_scanResults.size()-1 - i : i;
|
||||
|
||||
if (lockDeviceFrequency && !isInCurrentScanRange(m_scanResults[j].m_frequency)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
frequencySettings = m_settings.getFrequencySettings(m_scanResults[j].m_frequency);
|
||||
Real threshold = m_settings.getThreshold(frequencySettings);
|
||||
if (m_scanResults[i].m_power >= threshold)
|
||||
|
||||
if (checkThresholds(m_settings.m_voiceSquelchType, m_scanResults[j], threshold, m_settings.m_voiceSquelchThreshold))
|
||||
{
|
||||
frequency = m_scanResults[i].m_frequency;
|
||||
frequency = m_scanResults[j].m_frequency;
|
||||
activeFrequencySettings = frequencySettings;
|
||||
break;
|
||||
}
|
||||
@@ -501,20 +564,23 @@ void FreqScanner::processScanResults(const QDateTime& fftStartTime, const QList<
|
||||
}
|
||||
|
||||
// Ensure we have minimum offset from DC
|
||||
if (offset >= 0)
|
||||
if (!lockDeviceFrequency)
|
||||
{
|
||||
while (offset < m_settings.m_channelFrequencyOffset)
|
||||
if (offset >= 0)
|
||||
{
|
||||
nextCenterFrequency -= m_settings.m_channelBandwidth;
|
||||
offset += m_settings.m_channelBandwidth;
|
||||
while (offset < m_settings.m_channelFrequencyOffset)
|
||||
{
|
||||
nextCenterFrequency -= m_settings.m_channelBandwidth;
|
||||
offset += m_settings.m_channelBandwidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (abs(offset) < m_settings.m_channelFrequencyOffset)
|
||||
else
|
||||
{
|
||||
nextCenterFrequency += m_settings.m_channelBandwidth;
|
||||
offset -= m_settings.m_channelBandwidth;
|
||||
while (abs(offset) < m_settings.m_channelFrequencyOffset)
|
||||
{
|
||||
nextCenterFrequency += m_settings.m_channelBandwidth;
|
||||
offset -= m_settings.m_channelBandwidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +646,12 @@ void FreqScanner::processScanResults(const QDateTime& fftStartTime, const QList<
|
||||
}
|
||||
}
|
||||
|
||||
if (nextCenterFrequency != m_centerFrequency) {
|
||||
if (m_settings.m_lockDeviceFrequency) {
|
||||
nextCenterFrequency = m_centerFrequency;
|
||||
}
|
||||
|
||||
if (nextCenterFrequency != m_centerFrequency)
|
||||
{
|
||||
setDeviceCenterFrequency(nextCenterFrequency);
|
||||
}
|
||||
|
||||
@@ -604,7 +675,7 @@ void FreqScanner::processScanResults(const QDateTime& fftStartTime, const QList<
|
||||
// Wait until power drops below threshold
|
||||
FreqScannerSettings::FrequencySettings *frequencySettings = m_settings.getFrequencySettings(m_activeFrequency);
|
||||
Real threshold = m_settings.getThreshold(frequencySettings);
|
||||
if (results[i].m_power < threshold)
|
||||
if (results[i].m_power < threshold)
|
||||
{
|
||||
m_timeoutTimer.setSingleShot(true);
|
||||
m_timeoutTimer.start((int)(m_settings.m_retransmitTime * 1000.0));
|
||||
@@ -627,8 +698,12 @@ void FreqScanner::processScanResults(const QDateTime& fftStartTime, const QList<
|
||||
// Check if power has returned to being above threshold
|
||||
FreqScannerSettings::FrequencySettings *frequencySettings = m_settings.getFrequencySettings(m_activeFrequency);
|
||||
Real threshold = m_settings.getThreshold(frequencySettings);
|
||||
if (results[i].m_power >= threshold)
|
||||
|
||||
if (checkThresholds(m_settings.m_voiceSquelchType, results[i], threshold, m_settings.m_voiceSquelchThreshold))
|
||||
{
|
||||
if (m_settings.m_voiceSquelchType != FreqScannerSettings::VoiceSquelchType::None) {
|
||||
qDebug("FreqScanner::processScanResults: WAIT_FOR_RETRANSMISSION restart: frequency: %lld, voice score: %f", m_activeFrequency, results[i].m_voiceActivityLevel);
|
||||
}
|
||||
m_timeoutTimer.stop();
|
||||
m_state = WAIT_FOR_END_TX;
|
||||
}
|
||||
@@ -668,7 +743,13 @@ void FreqScanner::timeout()
|
||||
void FreqScanner::calcScannerSampleRate(int channelBW, int basebandSampleRate, int& scannerSampleRate, int& fftSize, int& binsPerChannel)
|
||||
{
|
||||
const int maxFFTSize = 16384;
|
||||
const int minBinsPerChannel = 8;
|
||||
int minBinsPerChannel = 8;
|
||||
|
||||
if (m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceSquelchType::VoiceLsb
|
||||
|| m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceSquelchType::VoiceUsb)
|
||||
{
|
||||
minBinsPerChannel = channelBW / 20; // we want at most 20 Hz per bin
|
||||
}
|
||||
|
||||
// Base FFT size on that used for main spectrum
|
||||
std::vector<DeviceSet*>& deviceSets = MainCore::instance()->getDeviceSets();
|
||||
@@ -787,6 +868,17 @@ void FreqScanner::unmuteAll()
|
||||
m_autoMutedChannels.clear();
|
||||
}
|
||||
|
||||
bool FreqScanner::checkThresholds(
|
||||
FreqScannerSettings::VoiceSquelchType voiceSquelchType,
|
||||
const FreqScanner::MsgScanResult::ScanResult& result,
|
||||
Real powerThreshold,
|
||||
Real voiceSquelchThreshold
|
||||
)
|
||||
{
|
||||
return (((result.m_power >= powerThreshold) && (voiceSquelchType == FreqScannerSettings::VoiceSquelchType::None))
|
||||
|| (result.m_voiceActivityLevel >= voiceSquelchThreshold));
|
||||
}
|
||||
|
||||
void FreqScanner::applyChannelSetting(const QString& channel)
|
||||
{
|
||||
if (!MainCore::getDeviceAndChannelIndexFromId(channel, m_scanDeviceSetIndex, m_scanChannelIndex)) {
|
||||
|
||||
@@ -146,6 +146,23 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class MsgContinueScan : public Message {
|
||||
MESSAGE_CLASS_DECLARATION
|
||||
|
||||
public:
|
||||
|
||||
static MsgContinueScan* create()
|
||||
{
|
||||
return new MsgContinueScan();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
MsgContinueScan() :
|
||||
Message()
|
||||
{
|
||||
}
|
||||
};
|
||||
class MsgScanResult : public Message {
|
||||
MESSAGE_CLASS_DECLARATION
|
||||
|
||||
@@ -154,9 +171,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<ScanResult>& getScanResults() { return m_scanResults; }
|
||||
|
||||
static MsgScanResult* create(const QDateTime& fftStartTime) {
|
||||
@@ -421,6 +439,7 @@ private:
|
||||
void startScan();
|
||||
void stopScan();
|
||||
void initScan();
|
||||
void continueScan();
|
||||
void processScanResults(const QDateTime& fftStartTime, const QList<MsgScanResult::ScanResult>& results);
|
||||
void setDeviceCenterFrequency(qint64 frequency);
|
||||
void applyChannelSetting(const QString& channel);
|
||||
@@ -428,6 +447,12 @@ private:
|
||||
void unmuteAll();
|
||||
void mute(unsigned int deviceSetIndex, unsigned int channelIndex);
|
||||
void unmute(unsigned int deviceSetIndex, unsigned int channelIndex);
|
||||
static bool checkThresholds(
|
||||
FreqScannerSettings::VoiceSquelchType voiceSquelchType,
|
||||
const FreqScanner::MsgScanResult::ScanResult& result,
|
||||
Real powerThreshold,
|
||||
Real voiceSquelchThreshold
|
||||
);
|
||||
|
||||
static QList<SWGSDRangel::SWGFreqScannerFrequency *> *createFrequencyList(const FreqScannerSettings& settings);
|
||||
|
||||
@@ -440,4 +465,3 @@ private slots:
|
||||
};
|
||||
|
||||
#endif // INCLUDE_FREQSCANNER_H
|
||||
|
||||
|
||||
@@ -168,6 +168,24 @@ void FreqScannerAddRangeDialog::on_preset_currentTextChanged(const QString& text
|
||||
{
|
||||
enableManAdjust = false;
|
||||
}
|
||||
else if (text == "HF 20m")
|
||||
{
|
||||
ui->start->setValue(14180500);
|
||||
ui->stop->setValue(14250500);
|
||||
ui->step->setCurrentText("1000");
|
||||
}
|
||||
else if (text == "HF 40m")
|
||||
{
|
||||
ui->start->setValue(7110500);
|
||||
ui->stop->setValue(7190500);
|
||||
ui->step->setCurrentText("1000");
|
||||
}
|
||||
else if (text == "HF 80m")
|
||||
{
|
||||
ui->start->setValue(3680500);
|
||||
ui->stop->setValue(3780500);
|
||||
ui->step->setCurrentText("1000");
|
||||
}
|
||||
ui->start->setEnabled(enableManAdjust);
|
||||
ui->stop->setEnabled(enableManAdjust);
|
||||
ui->step->setEnabled(enableManAdjust);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>385</width>
|
||||
<height>190</height>
|
||||
<height>203</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
@@ -130,6 +130,11 @@
|
||||
<string>100000</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1000</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
@@ -203,6 +208,21 @@
|
||||
<string>HF ATC</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>HF 20m</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>HF 40m</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>HF 80m</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
|
||||
@@ -29,7 +29,8 @@ MESSAGE_CLASS_DEFINITION(FreqScannerBaseband::MsgConfigureFreqScannerBaseband, M
|
||||
|
||||
FreqScannerBaseband::FreqScannerBaseband(FreqScanner *freqScanner) :
|
||||
m_freqScanner(freqScanner),
|
||||
m_messageQueueToGUI(nullptr)
|
||||
m_messageQueueToGUI(nullptr),
|
||||
m_currentBasebandSampleRate(0)
|
||||
{
|
||||
qDebug("FreqScannerBaseband::FreqScannerBaseband");
|
||||
|
||||
@@ -62,6 +63,7 @@ void FreqScannerBaseband::reset()
|
||||
m_inputMessageQueue.clear();
|
||||
m_sampleFifo.reset();
|
||||
m_channelSampleRate = 0;
|
||||
m_currentBasebandSampleRate = 0;
|
||||
}
|
||||
|
||||
void FreqScannerBaseband::setChannel(ChannelAPI *channel)
|
||||
@@ -130,11 +132,18 @@ bool FreqScannerBaseband::handleMessage(const Message& cmd)
|
||||
QMutexLocker mutexLocker(&m_mutex);
|
||||
DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
|
||||
qDebug() << "FreqScannerBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate();
|
||||
setBasebandSampleRate(notif.getSampleRate());
|
||||
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate()));
|
||||
if (m_channelSampleRate != m_channelizer->getChannelSampleRate()) {
|
||||
m_channelSampleRate = m_channelizer->getChannelSampleRate();
|
||||
int basebandSampleRate = notif.getSampleRate();
|
||||
|
||||
if (basebandSampleRate != m_currentBasebandSampleRate)
|
||||
{
|
||||
setBasebandSampleRate(notif.getSampleRate());
|
||||
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate()));
|
||||
if (m_channelSampleRate != m_channelizer->getChannelSampleRate()) {
|
||||
m_channelSampleRate = m_channelizer->getChannelSampleRate();
|
||||
}
|
||||
m_currentBasebandSampleRate = basebandSampleRate;
|
||||
}
|
||||
|
||||
m_sink.setCenterFrequency(notif.getCenterFrequency());
|
||||
|
||||
return true;
|
||||
|
||||
@@ -86,6 +86,7 @@ private:
|
||||
MessageQueue *m_messageQueueToGUI;
|
||||
FreqScannerSettings m_settings;
|
||||
QRecursiveMutex m_mutex;
|
||||
int m_currentBasebandSampleRate;
|
||||
|
||||
bool handleMessage(const Message& cmd);
|
||||
void applySettings(const FreqScannerSettings& settings, const QStringList& settingsKeys, bool force = false);
|
||||
|
||||
@@ -197,6 +197,8 @@ bool FreqScannerGUI::handleMessage(const Message& message)
|
||||
int row = item->row();
|
||||
QTableWidgetItem* powerItem = ui->table->item(row, COL_POWER);
|
||||
powerItem->setData(Qt::DisplayRole, results[i].m_power);
|
||||
QTableWidgetItem* vadItem = ui->table->item(row, COL_VAD);
|
||||
vadItem->setData(Qt::DisplayRole, results[i].m_voiceActivityLevel);
|
||||
FreqScannerSettings::FrequencySettings *frequencySettings = m_settings.getFrequencySettings(freq);
|
||||
Real threshold = m_settings.getThreshold(frequencySettings);
|
||||
bool active = results[i].m_power >= threshold;
|
||||
@@ -361,6 +363,50 @@ 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;
|
||||
QStringList settingsKeys({"voiceSquelchType"});
|
||||
|
||||
if (m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceSquelchType::VoiceLsb)
|
||||
{
|
||||
blockApplySettings(true);
|
||||
m_settings.m_channelBandwidth = 3000;
|
||||
ui->channelBandwidth->setValue(m_settings.m_channelBandwidth);
|
||||
m_settings.m_channelShift = 1500;
|
||||
ui->channelShift->setValue(m_settings.m_channelShift);
|
||||
settingsKeys.append("channelBandwidth");
|
||||
settingsKeys.append("channelShift");
|
||||
blockApplySettings(false);
|
||||
}
|
||||
else if (m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceSquelchType::VoiceUsb)
|
||||
{
|
||||
blockApplySettings(true);
|
||||
m_settings.m_channelBandwidth = 3000;
|
||||
ui->channelBandwidth->setValue(m_settings.m_channelBandwidth);
|
||||
m_settings.m_channelShift = -1500;
|
||||
ui->channelShift->setValue(m_settings.m_channelShift);
|
||||
settingsKeys.append("channelBandwidth");
|
||||
settingsKeys.append("channelShift");
|
||||
blockApplySettings(false);
|
||||
}
|
||||
|
||||
applySettings(settingsKeys);
|
||||
}
|
||||
|
||||
void FreqScannerGUI::on_lockDeviceFrequency_toggled(bool checked)
|
||||
{
|
||||
m_settings.m_lockDeviceFrequency = checked;
|
||||
applySetting("lockDeviceFrequency");
|
||||
}
|
||||
|
||||
void FreqScannerGUI::on_priority_currentIndexChanged(int index)
|
||||
{
|
||||
m_settings.m_priority = (FreqScannerSettings::Priority)index;
|
||||
@@ -540,6 +586,7 @@ FreqScannerGUI::FreqScannerGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, B
|
||||
|
||||
ui->table->setItemDelegateForColumn(COL_FREQUENCY, new FrequencyDelegate("Auto", 3, true, ui->table));
|
||||
ui->table->setItemDelegateForColumn(COL_POWER, new DecimalDelegate(1, ui->table));
|
||||
ui->table->setItemDelegateForColumn(COL_VAD, new DecimalDelegate(2, ui->table));
|
||||
ui->table->setItemDelegateForColumn(COL_CHANNEL_BW, new Int64Delegate(0, 10000000, ui->table));
|
||||
ui->table->setItemDelegateForColumn(COL_TH, new DecimalDelegate(1, -120.0, 0.0, ui->table));
|
||||
ui->table->setItemDelegateForColumn(COL_SQ, new DecimalDelegate(1, -120.0, 0.0, ui->table));
|
||||
@@ -597,6 +644,7 @@ void FreqScannerGUI::displaySettings()
|
||||
ui->channels->setCurrentIndex(channelIndex);
|
||||
}
|
||||
ui->deltaFrequency->setValue(m_settings.m_channelFrequencyOffset);
|
||||
ui->deviceFreqLock->setChecked(m_settings.m_lockDeviceFrequency);
|
||||
ui->channelBandwidth->setValue(m_settings.m_channelBandwidth);
|
||||
ui->channelShift->setValue(m_settings.m_channelShift);
|
||||
ui->scanTime->setValue(m_settings.m_scanTime * 10.0);
|
||||
@@ -607,6 +655,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);
|
||||
@@ -666,6 +717,12 @@ void FreqScannerGUI::on_startStop_toggled(bool checked)
|
||||
}
|
||||
}
|
||||
|
||||
void FreqScannerGUI::on_continueScan_clicked()
|
||||
{
|
||||
FreqScanner::MsgContinueScan* message = FreqScanner::MsgContinueScan::create();
|
||||
m_freqScanner->getInputMessageQueue()->push(message);
|
||||
}
|
||||
|
||||
void FreqScannerGUI::addRow(const FreqScannerSettings::FrequencySettings& frequencySettings)
|
||||
{
|
||||
int row = ui->table->rowCount();
|
||||
@@ -687,6 +744,10 @@ void FreqScannerGUI::addRow(const FreqScannerSettings::FrequencySettings& freque
|
||||
powerItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
ui->table->setItem(row, COL_POWER, powerItem);
|
||||
|
||||
QTableWidgetItem* vadItem = new QTableWidgetItem();
|
||||
vadItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
ui->table->setItem(row, COL_VAD, vadItem);
|
||||
|
||||
QTableWidgetItem *activeCountItem = new QTableWidgetItem();
|
||||
activeCountItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
ui->table->setItem(row, COL_ACTIVE_COUNT, activeCountItem);
|
||||
@@ -776,6 +837,13 @@ void FreqScannerGUI::on_removeInactive_clicked()
|
||||
applySetting("frequencySettings");
|
||||
}
|
||||
|
||||
void FreqScannerGUI::on_removeAll_clicked()
|
||||
{
|
||||
ui->table->setRowCount(0);
|
||||
m_settings.m_frequencySettings.clear();
|
||||
applySetting("frequencySettings");
|
||||
}
|
||||
|
||||
static QList<QTableWidgetItem*> takeRow(QTableWidget* table, int row)
|
||||
{
|
||||
QList<QTableWidgetItem*> rowItems;
|
||||
@@ -1236,6 +1304,7 @@ void FreqScannerGUI::resizeTable()
|
||||
ui->table->setItem(row, COL_ANNOTATION, new QTableWidgetItem("London VOLMET"));
|
||||
ui->table->setItem(row, COL_ENABLE, new QTableWidgetItem("Enable"));
|
||||
ui->table->setItem(row, COL_POWER, new QTableWidgetItem("-100.0"));
|
||||
ui->table->setItem(row, COL_VAD, new QTableWidgetItem("0.00"));
|
||||
ui->table->setItem(row, COL_ACTIVE_COUNT, new QTableWidgetItem("10000"));
|
||||
ui->table->setItem(row, COL_NOTES, new QTableWidgetItem("A channel name"));
|
||||
ui->table->setItem(row, COL_CHANNEL, new QTableWidgetItem("Enter some notes"));
|
||||
@@ -1256,15 +1325,20 @@ 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<int>::of(&QComboBox::currentIndexChanged), this, &FreqScannerGUI::on_voiceSquelchType_currentIndexChanged);
|
||||
QObject::connect(ui->voiceThreshold, &QDial::valueChanged, this, &FreqScannerGUI::on_voiceThreshold_valueChanged);
|
||||
QObject::connect(ui->deviceFreqLock, &QToolButton::toggled, this, &FreqScannerGUI::on_lockDeviceFrequency_toggled);
|
||||
QObject::connect(ui->priority, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &FreqScannerGUI::on_priority_currentIndexChanged);
|
||||
QObject::connect(ui->measurement, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &FreqScannerGUI::on_measurement_currentIndexChanged);
|
||||
QObject::connect(ui->mode, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &FreqScannerGUI::on_mode_currentIndexChanged);
|
||||
QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &FreqScannerGUI::on_startStop_toggled);
|
||||
QObject::connect(ui->continueScan, &QPushButton::clicked, this, &FreqScannerGUI::on_continueScan_clicked);
|
||||
QObject::connect(ui->table, &QTableWidget::cellChanged, this, &FreqScannerGUI::on_table_cellChanged);
|
||||
QObject::connect(ui->addSingle, &QToolButton::clicked, this, &FreqScannerGUI::on_addSingle_clicked);
|
||||
QObject::connect(ui->addRange, &QToolButton::clicked, this, &FreqScannerGUI::on_addRange_clicked);
|
||||
QObject::connect(ui->remove, &QToolButton::clicked, this, &FreqScannerGUI::on_remove_clicked);
|
||||
QObject::connect(ui->removeInactive, &QToolButton::clicked, this, &FreqScannerGUI::on_removeInactive_clicked);
|
||||
QObject::connect(ui->removeAll, &QToolButton::clicked, this, &FreqScannerGUI::on_removeAll_clicked);
|
||||
QObject::connect(ui->up, &QToolButton::clicked, this, &FreqScannerGUI::on_up_clicked);
|
||||
QObject::connect(ui->down, &QToolButton::clicked, this, &FreqScannerGUI::on_down_clicked);
|
||||
QObject::connect(ui->clearActiveCount, &QToolButton::clicked, this, &FreqScannerGUI::on_clearActiveCount_clicked);
|
||||
|
||||
@@ -115,6 +115,7 @@ private:
|
||||
COL_ANNOTATION,
|
||||
COL_ENABLE,
|
||||
COL_POWER,
|
||||
COL_VAD,
|
||||
COL_ACTIVE_COUNT,
|
||||
COL_NOTES,
|
||||
COL_CHANNEL,
|
||||
@@ -132,6 +133,9 @@ 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_lockDeviceFrequency_toggled(bool checked);
|
||||
void on_priority_currentIndexChanged(int index);
|
||||
void on_measurement_currentIndexChanged(int index);
|
||||
void on_mode_currentIndexChanged(int index);
|
||||
@@ -143,10 +147,12 @@ private slots:
|
||||
void columnSelectMenu(QPoint pos);
|
||||
void columnSelectMenuChecked(bool checked = false);
|
||||
void on_startStop_toggled(bool checked = false);
|
||||
void on_continueScan_clicked();
|
||||
void on_addSingle_clicked();
|
||||
void on_addRange_clicked();
|
||||
void on_remove_clicked();
|
||||
void on_removeInactive_clicked();
|
||||
void on_removeAll_clicked();
|
||||
void on_up_clicked();
|
||||
void on_down_clicked();
|
||||
void on_clearActiveCount_clicked();
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>516</width>
|
||||
<height>423</height>
|
||||
<width>676</width>
|
||||
<height>410</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@@ -39,7 +39,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>511</width>
|
||||
<width>671</width>
|
||||
<height>411</height>
|
||||
</rect>
|
||||
</property>
|
||||
@@ -157,6 +157,24 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="deviceFreqLock">
|
||||
<property name="toolTip">
|
||||
<string>Lock device frequency to current value</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../sdrgui/resources/res.qrc">
|
||||
<normaloff>:/unlocked.png</normaloff>
|
||||
<normalon>:/locked.png</normalon>:/unlocked.png</iconset>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
@@ -166,6 +184,9 @@
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
@@ -259,6 +280,9 @@
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
@@ -526,6 +550,9 @@
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
@@ -650,6 +677,26 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="continueScan">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Force scanner to continue on next frequency</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../sdrgui/resources/res.qrc">
|
||||
<normaloff>:/arrow_left.png</normaloff>:/arrow_left.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="status">
|
||||
<property name="sizePolicy">
|
||||
@@ -663,6 +710,67 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="voiceSquelchLabel">
|
||||
<property name="text">
|
||||
<string>VAD</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="voiceSquelch">
|
||||
<property name="toolTip">
|
||||
<string>Voice Activity Detection</string>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>None</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>LSB</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>USB</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDial" name="voiceThreshold">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>24</width>
|
||||
<height>24</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>VAD score minimum level</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="pageStep">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>50</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="voiceThresholdText">
|
||||
<property name="text">
|
||||
<string>0.01</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
@@ -705,6 +813,14 @@
|
||||
<string>Channel power in decibels during the previous scan</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>VAD</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Voice level</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Active Count</string>
|
||||
@@ -805,6 +921,16 @@ Leave blank for no adjustment</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="removeAll">
|
||||
<property name="toolTip">
|
||||
<string>Remove selected items</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove All</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="up">
|
||||
<property name="toolTip">
|
||||
@@ -863,6 +989,9 @@ Leave blank for no adjustment</string>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
@@ -898,18 +1027,18 @@ Leave blank for no adjustment</string>
|
||||
<extends>QToolButton</extends>
|
||||
<header>gui/buttonswitch.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ValueDialZ</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>gui/valuedialz.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>RollupContents</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>gui/rollupcontents.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ValueDialZ</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>gui/valuedialz.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>deltaFrequency</tabstop>
|
||||
|
||||
@@ -46,6 +46,9 @@ void FreqScannerSettings::resetToDefaults()
|
||||
m_scanTime = 0.1f;
|
||||
m_retransmitTime = 2.0f;
|
||||
m_tuneTime = 100;
|
||||
m_voiceSquelchThreshold = 0.5f;
|
||||
m_voiceSquelchType = None;
|
||||
m_lockDeviceFrequency = false;
|
||||
m_priority = MAX_POWER;
|
||||
m_measurement = PEAK;
|
||||
m_mode = CONTINUOUS;
|
||||
@@ -76,6 +79,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);
|
||||
@@ -85,6 +90,7 @@ QByteArray FreqScannerSettings::serialize() const
|
||||
s.writeS32(14, (int)m_mode);
|
||||
s.writeList(15, m_frequencySettings);
|
||||
s.writeS32(16, m_channelShift);
|
||||
s.writeBool(17, m_lockDeviceFrequency);
|
||||
|
||||
s.writeList(20, m_columnIndexes);
|
||||
s.writeList(21, m_columnSizes);
|
||||
@@ -130,6 +136,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);
|
||||
@@ -139,6 +147,7 @@ bool FreqScannerSettings::deserialize(const QByteArray& data)
|
||||
d.readS32(14, (int*)&m_mode, (int)CONTINUOUS);
|
||||
d.readList(15, &m_frequencySettings);
|
||||
d.readS32(16, &m_channelShift, 0);
|
||||
d.readBool(17, &m_lockDeviceFrequency, false);
|
||||
|
||||
if (m_frequencySettings.size() == 0)
|
||||
{
|
||||
@@ -222,6 +231,15 @@ 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("lockDeviceFrequency")) {
|
||||
m_lockDeviceFrequency = settings.m_lockDeviceFrequency;
|
||||
}
|
||||
if (settingsKeys.contains("frequencySettings")) {
|
||||
m_frequencySettings = settings.m_frequencySettings;
|
||||
}
|
||||
@@ -303,6 +321,15 @@ 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("lockDeviceFrequency") || force) {
|
||||
ostr << " m_lockDeviceFrequency: " << m_lockDeviceFrequency;
|
||||
}
|
||||
if (settingsKeys.contains("frequencySettings") || force)
|
||||
{
|
||||
QStringList s;
|
||||
|
||||
@@ -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<FrequencySettings> 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
|
||||
@@ -68,6 +74,8 @@ struct FreqScannerSettings
|
||||
SCAN_ONLY,
|
||||
MULTIPLEX
|
||||
} m_mode; //!< Whether to run a single or many scans
|
||||
bool m_lockDeviceFrequency; //!< Whether to lock device frequency to the initial center frequency of the first scan,
|
||||
//!< or allow it to be shifted by the channel shift setting
|
||||
|
||||
QList<int> m_columnIndexes;//!< How the columns are ordered in the table
|
||||
QList<int> m_columnSizes; //!< Size of the coumns in the table
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
#include <QDebug>
|
||||
|
||||
#include <complex.h>
|
||||
#include <cmath>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
#include "dsp/dspengine.h"
|
||||
#include "dsp/fftfactory.h"
|
||||
@@ -38,7 +43,12 @@ FreqScannerSink::FreqScannerSink() :
|
||||
m_fftCounter(0),
|
||||
m_fftSize(1024),
|
||||
m_binsPerChannel(16),
|
||||
m_averageCount(0)
|
||||
m_averageCount(0),
|
||||
m_cepstrumSequenceInverse(-1),
|
||||
m_cepstrumSequenceForward(-1),
|
||||
m_cepstrumFFTInverse(nullptr),
|
||||
m_cepstrumFFTForward(nullptr),
|
||||
m_cepstrumSize(0)
|
||||
{
|
||||
applySettings(m_settings, QStringList(), true);
|
||||
applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, 16, 4, true);
|
||||
@@ -46,11 +56,19 @@ FreqScannerSink::FreqScannerSink() :
|
||||
|
||||
FreqScannerSink::~FreqScannerSink()
|
||||
{
|
||||
if (m_fftSequence >= 0)
|
||||
{
|
||||
FFTFactory* fftFactory = DSPEngine::instance()->getFFTFactory();
|
||||
FFTFactory* fftFactory = DSPEngine::instance()->getFFTFactory();
|
||||
|
||||
if (m_fftSequence >= 0) {
|
||||
fftFactory->releaseEngine(m_fftSize, false, m_fftSequence);
|
||||
}
|
||||
|
||||
if (m_cepstrumSequenceInverse >= 0) {
|
||||
fftFactory->releaseEngine(m_cepstrumSize, true, m_cepstrumSequenceInverse);
|
||||
}
|
||||
|
||||
if (m_cepstrumSequenceForward >= 0) {
|
||||
fftFactory->releaseEngine(m_cepstrumSize, false, m_cepstrumSequenceForward);
|
||||
}
|
||||
}
|
||||
|
||||
void FreqScannerSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end)
|
||||
@@ -95,6 +113,52 @@ void FreqScannerSink::processOneSample(Complex &ci)
|
||||
// Perform FFT
|
||||
m_fft->transform();
|
||||
|
||||
// Accumulate voice activity levels on individual FFT (before averaging)
|
||||
// This captures sharp formant structure better than averaged spectrum
|
||||
int freqCount = m_settings.m_frequencySettings.size();
|
||||
if (m_voiceLevelSum.size() != freqCount) {
|
||||
m_voiceLevelSum.resize(freqCount);
|
||||
m_voiceLevelCount.resize(freqCount);
|
||||
m_voiceLevelSum.fill(0.0);
|
||||
m_voiceLevelCount.fill(0);
|
||||
}
|
||||
|
||||
for (int i = 0; i < freqCount; i++)
|
||||
{
|
||||
if (m_settings.m_frequencySettings[i].m_enabled)
|
||||
{
|
||||
qint64 frequency = m_settings.m_frequencySettings[i].m_frequency;
|
||||
qint64 startFrequency = m_centerFrequency - m_scannerSampleRate / 2;
|
||||
qint64 diff = frequency - startFrequency;
|
||||
float binBW = m_scannerSampleRate / (float)m_fftSize;
|
||||
|
||||
// avoid spectrum edges where there may be aliasing from half-band filters
|
||||
if ((diff >= m_scannerSampleRate / 8) && (diff < m_scannerSampleRate * 7 / 8))
|
||||
{
|
||||
int bin = std::round(diff / binBW);
|
||||
int channelBins;
|
||||
if (m_settings.m_frequencySettings[i].m_channelBandwidth.isEmpty()) {
|
||||
channelBins = m_binsPerChannel;
|
||||
} else {
|
||||
int channelBW = m_settings.getChannelBandwidth(&m_settings.m_frequencySettings[i]);
|
||||
channelBins = m_fftSize / (m_scannerSampleRate / (float)channelBW);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (voiceLevel > 0.0) {
|
||||
m_voiceLevelSum[i] += voiceLevel;
|
||||
m_voiceLevelCount[i]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder (so negative frequencies are first) and average
|
||||
int halfSize = m_fftSize / 2;
|
||||
for (int i = 0; i < halfSize; i++) {
|
||||
@@ -124,8 +188,10 @@ void FreqScannerSink::processOneSample(Complex &ci)
|
||||
// Ignore results in upper and lower 12.5%, as there may be aliasing here from half-band filters
|
||||
if ((diff >= m_scannerSampleRate / 8) && (diff < m_scannerSampleRate * 7 / 8))
|
||||
{
|
||||
int bin = std::round(diff / binBW);
|
||||
int channelBins;
|
||||
int bin = std::round(diff / binBW); // Bin corresponding to the frequency
|
||||
int channelBins; // Number of bins in the channel containing the frequency.
|
||||
// This is either the default (m_binsPerChannel)
|
||||
// or calculated based on the channel bandwidth if specified in settings for this frequency
|
||||
|
||||
if (m_settings.m_frequencySettings[i].m_channelBandwidth.isEmpty())
|
||||
{
|
||||
@@ -144,8 +210,22 @@ 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};
|
||||
|
||||
// Use averaged voice activity level from individual FFTs
|
||||
Real voiceLevel = 0.0;
|
||||
if ((m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceLsb ||
|
||||
m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceUsb) &&
|
||||
m_voiceLevelCount[i] > 0)
|
||||
{
|
||||
voiceLevel = m_voiceLevelSum[i] / m_voiceLevelCount[i];
|
||||
if (voiceLevel > m_settings.m_voiceSquelchThreshold) {
|
||||
qDebug() << "FreqScannerSink::processOneSample: freq"
|
||||
<< frequency + (m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceLsb ? 1500 : -1500)
|
||||
<< "voiceLevel" << voiceLevel << "count" << m_voiceLevelCount[i];
|
||||
}
|
||||
}
|
||||
|
||||
FreqScanner::MsgScanResult::ScanResult result = {frequency, power, voiceLevel};
|
||||
results.append(result);
|
||||
}
|
||||
}
|
||||
@@ -154,6 +234,10 @@ void FreqScannerSink::processOneSample(Complex &ci)
|
||||
}
|
||||
m_averageCount = 0;
|
||||
m_fftStartTime = QDateTime::currentDateTime();
|
||||
|
||||
// Reset voice level accumulators for next averaging period
|
||||
m_voiceLevelSum.fill(0.0);
|
||||
m_voiceLevelCount.fill(0);
|
||||
}
|
||||
m_fftCounter = 0;
|
||||
}
|
||||
@@ -205,6 +289,24 @@ Real FreqScannerSink::magSq(int bin) const
|
||||
return magsq;
|
||||
}
|
||||
|
||||
// Compute magSq from raw FFT output with reordering (negative frequencies first)
|
||||
Real FreqScannerSink::magSqFromRawFFT(int bin) const
|
||||
{
|
||||
// m_magSq is reordered: negative freqs first, then positive
|
||||
// m_fft->out() is in standard FFT order: DC, positive freqs, negative freqs
|
||||
int halfSize = m_fftSize / 2;
|
||||
int fftBin;
|
||||
if (bin < halfSize) {
|
||||
// Negative frequencies: map to second half of FFT output
|
||||
fftBin = bin + halfSize;
|
||||
} else {
|
||||
// Positive frequencies: map to first half of FFT output
|
||||
fftBin = bin - halfSize;
|
||||
}
|
||||
return magSq(fftBin);
|
||||
}
|
||||
|
||||
|
||||
void FreqScannerSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, int scannerSampleRate, int fftSize, int binsPerChannel, bool force)
|
||||
{
|
||||
qDebug() << "FreqScannerSink::applyChannelSettings:"
|
||||
@@ -231,7 +333,7 @@ void FreqScannerSink::applyChannelSettings(int channelSampleRate, int channelFre
|
||||
{
|
||||
FFTFactory* fftFactory = DSPEngine::instance()->getFFTFactory();
|
||||
if (m_fftSequence >= 0) {
|
||||
fftFactory->releaseEngine(fftSize, false, m_fftSequence);
|
||||
fftFactory->releaseEngine(m_fftSize, false, m_fftSequence);
|
||||
}
|
||||
m_fftSequence = fftFactory->getEngine(fftSize, false, &m_fft);
|
||||
m_fftCounter = 0;
|
||||
@@ -241,6 +343,39 @@ void FreqScannerSink::applyChannelSettings(int channelSampleRate, int channelFre
|
||||
int averages = m_settings.m_scanTime * scannerSampleRate / 2 / fftSize;
|
||||
m_fftAverage.resize(fftSize, averages);
|
||||
m_magSq.resize(fftSize);
|
||||
|
||||
// Resize voice level accumulators to match frequency count
|
||||
int freqCount = m_settings.m_frequencySettings.size();
|
||||
m_voiceLevelSum.resize(freqCount);
|
||||
m_voiceLevelCount.resize(freqCount);
|
||||
m_voiceLevelSum.fill(0.0);
|
||||
m_voiceLevelCount.fill(0);
|
||||
|
||||
// Allocate cepstral FFT engines for formant detection
|
||||
// Size needs to be power-of-2 and >= channel bins (typically ~100-200 bins)
|
||||
// Use conservative size to handle most SSB channels
|
||||
int maxChannelBins = std::max(binsPerChannel * 2, 256);
|
||||
int cepstrumSize = 1;
|
||||
while (cepstrumSize < maxChannelBins) {
|
||||
cepstrumSize <<= 1;
|
||||
}
|
||||
|
||||
if (m_cepstrumSize != cepstrumSize) {
|
||||
// Release old engines if size changed
|
||||
if (m_cepstrumSequenceInverse >= 0) {
|
||||
fftFactory->releaseEngine(m_cepstrumSize, true, m_cepstrumSequenceInverse);
|
||||
}
|
||||
if (m_cepstrumSequenceForward >= 0) {
|
||||
fftFactory->releaseEngine(m_cepstrumSize, false, m_cepstrumSequenceForward);
|
||||
}
|
||||
|
||||
// Allocate new engines
|
||||
m_cepstrumSequenceInverse = fftFactory->getEngine(cepstrumSize, true, &m_cepstrumFFTInverse);
|
||||
m_cepstrumSequenceForward = fftFactory->getEngine(cepstrumSize, false, &m_cepstrumFFTForward);
|
||||
m_cepstrumSize = cepstrumSize;
|
||||
|
||||
qDebug() << "FreqScannerSink::applyChannelSettings: Allocated cepstral FFT engines, size:" << cepstrumSize;
|
||||
}
|
||||
}
|
||||
|
||||
m_channelSampleRate = channelSampleRate;
|
||||
@@ -268,3 +403,649 @@ 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 using spectral smoothing
|
||||
// Returns a value from 0.0 (no voice) to 1.0 (strong voice signature)
|
||||
Real FreqScannerSink::voiceActivityLevel(int bin, int channelBins, bool isLSB)
|
||||
{
|
||||
// Voice band in SSB is typically 100-3000 Hz from carrier
|
||||
// We look for 2-4 formant peaks in the smoothed spectral envelope
|
||||
|
||||
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 carrierBin = isLSB ? endBin : startBin;
|
||||
|
||||
// Get formant envelope using spectral smoothing
|
||||
// This separates vocal tract resonances from pitch harmonics
|
||||
QVector<Real> formantEnvelope;
|
||||
Real pitchHz = 0.0;
|
||||
getFormantEnvelope(startBin, endBin, formantEnvelope, &pitchHz);
|
||||
|
||||
if (formantEnvelope.isEmpty()) {
|
||||
qDebug() << "FreqScannerSink::voiceActivityLevel formantEnvelope is empty!";
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Calculate noise floor from formant envelope
|
||||
Real noiseFloor = 0.0;
|
||||
Real maxEnv = 0.0;
|
||||
for (const Real env : formantEnvelope) {
|
||||
noiseFloor += env;
|
||||
maxEnv = std::max(maxEnv, env);
|
||||
}
|
||||
noiseFloor = formantEnvelope.size() > 0 ? noiseFloor / formantEnvelope.size() : 1e-12;
|
||||
|
||||
// For voice, we need reasonably high peaks relative to noise
|
||||
// Use 4 dB (2.5x) above average as threshold for peak detection
|
||||
// Additional validation: peak-to-noise ratio
|
||||
// If max is not sufficiently higher than average, signal quality is poor
|
||||
float peakToNoiseRatio = maxEnv / noiseFloor;
|
||||
|
||||
// Lower threshold: require at least 1.2x peak-to-noise ratio (only ~1.6dB)
|
||||
// After heavy smoothing, formants appear as gentle bumps, not sharp peaks
|
||||
if (peakToNoiseRatio < 1.2) {
|
||||
// Signal is essentially all noise
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// For peak detection, use much lower threshold since smoothed spectral peaks are gentle
|
||||
// Use 1.1x noise floor to catch formant peaks in the smoothed envelope
|
||||
Real threshold = noiseFloor * 1.1;
|
||||
|
||||
// Find formant peaks in the smoothed envelope
|
||||
QVector<int> formantBins;
|
||||
QVector<Real> formantMags;
|
||||
|
||||
// Simple peak detection in formant envelope
|
||||
const qsizetype formantEnvelopeSize = formantEnvelope.size();
|
||||
for (qsizetype i = 1; i + 1 < formantEnvelopeSize; ++i)
|
||||
{
|
||||
Real prev = formantEnvelope[i - 1];
|
||||
Real curr = formantEnvelope[i];
|
||||
Real next = formantEnvelope[i + 1];
|
||||
|
||||
// Local maximum above threshold
|
||||
if (curr > prev && curr > next && curr > threshold)
|
||||
{
|
||||
int absBin = startBin + static_cast<int>(i);
|
||||
// Use SIGNED offset to distinguish USB from LSB and reject mistuned signals
|
||||
// USB: formants at positive offset (100-3000 Hz above carrier)
|
||||
// LSB: formants at negative offset (-3000 to -100 Hz below carrier)
|
||||
float freqOffset = (absBin - carrierBin) * binBW;
|
||||
|
||||
// Check appropriate sideband for voice: USB uses positive, LSB uses negative
|
||||
// This rejects signals tuned to wrong sideband (e.g., 1kHz offset)
|
||||
bool inVoiceBand = isLSB ? (freqOffset <= -100.0 && freqOffset >= -3000.0)
|
||||
: (freqOffset >= 100.0 && freqOffset <= 3000.0);
|
||||
|
||||
if (inVoiceBand)
|
||||
{
|
||||
formantBins.append(absBin);
|
||||
formantMags.append(curr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Voice requires 2-4 formants
|
||||
if (formantBins.size() < 2) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Sort formants by frequency offset (not bin order) to handle LSB reversal
|
||||
// Convert to absolute frequencies since F1/F2 validation expects positive values
|
||||
QVector<float> formantFreqs;
|
||||
QVector<int> formantIndices;
|
||||
const qsizetype formantBinsSize = formantBins.size();
|
||||
|
||||
for (qsizetype i = 0; i < formantBinsSize; ++i)
|
||||
{
|
||||
// Use absolute value for formant frequency analysis (F1, F2 ranges are defined as positive)
|
||||
float freqOffset = std::abs(formantBins[i] - carrierBin) * binBW;
|
||||
formantFreqs.append(freqOffset);
|
||||
formantIndices.append(static_cast<int>(i));
|
||||
}
|
||||
|
||||
// Simple insertion sort by frequency
|
||||
const qsizetype formantFreqsSize = formantFreqs.size();
|
||||
for (qsizetype i = 1; i < formantFreqsSize; ++i)
|
||||
{
|
||||
for (qsizetype j = i; j > 0 && formantFreqs[j] < formantFreqs[j - 1]; --j)
|
||||
{
|
||||
std::swap(formantFreqs[j], formantFreqs[j - 1]);
|
||||
std::swap(formantIndices[j], formantIndices[j - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge peaks that are too close together (within 400 Hz)
|
||||
// Real formants have minimum separation of 400-500 Hz in SSB voice
|
||||
// So anything closer is ripple within a single formant
|
||||
// We keep the peak with highest magnitude and remove others
|
||||
QVector<float> mergedFormantFreqs;
|
||||
QVector<int> mergedFormantIndices;
|
||||
|
||||
const float minFormantSpacing = 400.0; // Hz - minimum spacing between real formants
|
||||
const qsizetype sortedFormantFreqsSize = formantFreqs.size();
|
||||
|
||||
for (qsizetype i = 0; i < sortedFormantFreqsSize; ++i)
|
||||
{
|
||||
if (i == 0 || formantFreqs[i] - mergedFormantFreqs.back() >= minFormantSpacing)
|
||||
{
|
||||
// This is a new formant (far enough from previous)
|
||||
mergedFormantFreqs.append(formantFreqs[i]);
|
||||
mergedFormantIndices.append(formantIndices[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This peak is too close to the previous one - merge by keeping highest magnitude
|
||||
int prevIdx = mergedFormantIndices.back();
|
||||
int currIdx = formantIndices[i];
|
||||
if (formantMags[currIdx] > formantMags[prevIdx])
|
||||
{
|
||||
// Replace with higher magnitude peak
|
||||
mergedFormantFreqs.back() = formantFreqs[i];
|
||||
mergedFormantIndices.back() = currIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formantFreqs = mergedFormantFreqs;
|
||||
formantIndices = mergedFormantIndices;
|
||||
|
||||
// After merging, we should still have at least 2 formants for voice
|
||||
if (formantFreqs.size() < 2) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Check formant spacing (voice formants should be 400-1500 Hz apart)
|
||||
// F1-F2 spacing is typically 600-1200 Hz
|
||||
bool goodSpacing = false;
|
||||
const qsizetype mergedFormantFreqsSize = formantFreqs.size();
|
||||
for (qsizetype i = 0; i + 1 < mergedFormantFreqsSize; ++i)
|
||||
{
|
||||
float spacing = formantFreqs[i + 1] - formantFreqs[i];
|
||||
if (spacing >= 400.0 && spacing <= 1500.0) {
|
||||
goodSpacing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!goodSpacing) {
|
||||
return 0.0; // Formants too close or too far apart
|
||||
}
|
||||
|
||||
// Check for F1 formant in expected range (300-1000 Hz from carrier)
|
||||
// This is critical for voice detection
|
||||
// F1 should be the lowest frequency formant
|
||||
bool hasF1 = false;
|
||||
Real f1Mag = 0.0;
|
||||
int f1Idx = -1;
|
||||
|
||||
if (formantFreqs.size() > 0 && formantFreqs[0] >= 300.0 && formantFreqs[0] <= 1000.0)
|
||||
{
|
||||
hasF1 = true;
|
||||
f1Idx = formantIndices[0];
|
||||
f1Mag = formantMags[f1Idx];
|
||||
}
|
||||
|
||||
if (!hasF1) {
|
||||
return 0.0; // No F1 formant - not voice or mistuned
|
||||
}
|
||||
|
||||
// Check for F2 formant in expected range (900-2500 Hz from carrier)
|
||||
// F2 should be higher frequency than F1 AND 400-1500 Hz away from F1
|
||||
bool hasF2 = false;
|
||||
Real f2Mag = 0.0;
|
||||
int f2Idx = -1;
|
||||
float f2Freq = 0.0;
|
||||
const qsizetype validatedFormantFreqsSize = formantFreqs.size();
|
||||
|
||||
for (qsizetype i = 1; i < validatedFormantFreqsSize; ++i)
|
||||
{
|
||||
float freqOffset = formantFreqs[i];
|
||||
float f1ToF2Spacing = freqOffset - formantFreqs[0];
|
||||
|
||||
// F2 must be: in range, higher than F1, and properly spaced from F1
|
||||
if (freqOffset >= 900.0 && freqOffset <= 2500.0 &&
|
||||
f1ToF2Spacing >= 400.0 && f1ToF2Spacing <= 1500.0)
|
||||
{
|
||||
hasF2 = true;
|
||||
f2Idx = formantIndices[i];
|
||||
f2Freq = freqOffset;
|
||||
f2Mag = formantMags[f2Idx];
|
||||
break; // Take the first valid F2
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasF2) {
|
||||
return 0.0; // No F2 formant - not voice or mistuned
|
||||
}
|
||||
|
||||
// Reject strongly tonal spectra (typical CW / single-tone signals).
|
||||
// Voice in SSB should have broader spectral spread, lower peak dominance,
|
||||
// and non-negligible spectral flatness across the 100-3000 Hz voice band.
|
||||
Real voiceBandTotalEnergy = 0.0;
|
||||
Real voiceBandPeakEnergy = 0.0;
|
||||
Real voiceBandSecondPeakEnergy = 0.0;
|
||||
Real voiceBandLogEnergySum = 0.0;
|
||||
Real voiceBandWeightedFreqSum = 0.0;
|
||||
Real voiceBandWeightedFreqSqSum = 0.0;
|
||||
QVector<Real> voiceBandEnergies;
|
||||
voiceBandEnergies.reserve(std::max(0, endBin - startBin + 1));
|
||||
int voiceBandBins = 0;
|
||||
|
||||
for (int absBin = startBin; absBin <= endBin; absBin++)
|
||||
{
|
||||
float signedOffset = (absBin - carrierBin) * binBW;
|
||||
float voiceOffset = isLSB ? -signedOffset : signedOffset;
|
||||
|
||||
if (voiceOffset < 100.0f || voiceOffset > 3000.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Real binEnergy = magSqFromRawFFT(absBin);
|
||||
Real safeEnergy = std::max(binEnergy, (Real) 1e-20);
|
||||
voiceBandEnergies.append(safeEnergy);
|
||||
|
||||
voiceBandTotalEnergy += safeEnergy;
|
||||
|
||||
if (safeEnergy > voiceBandPeakEnergy)
|
||||
{
|
||||
voiceBandSecondPeakEnergy = voiceBandPeakEnergy;
|
||||
voiceBandPeakEnergy = safeEnergy;
|
||||
}
|
||||
else if (safeEnergy > voiceBandSecondPeakEnergy)
|
||||
{
|
||||
voiceBandSecondPeakEnergy = safeEnergy;
|
||||
}
|
||||
|
||||
voiceBandLogEnergySum += std::log(safeEnergy);
|
||||
voiceBandWeightedFreqSum += safeEnergy * voiceOffset;
|
||||
voiceBandWeightedFreqSqSum += safeEnergy * voiceOffset * voiceOffset;
|
||||
voiceBandBins++;
|
||||
}
|
||||
|
||||
if (voiceBandBins <= 0 || voiceBandTotalEnergy <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Real voiceBandMeanEnergy = voiceBandTotalEnergy / voiceBandBins;
|
||||
Real voiceBandGeometricMean = std::exp(voiceBandLogEnergySum / voiceBandBins);
|
||||
Real voiceBandMeanFreq = voiceBandWeightedFreqSum / voiceBandTotalEnergy;
|
||||
Real voiceBandMeanFreqSq = voiceBandWeightedFreqSqSum / voiceBandTotalEnergy;
|
||||
Real voiceBandVariance = std::max((Real) 0.0, voiceBandMeanFreqSq - voiceBandMeanFreq * voiceBandMeanFreq);
|
||||
float voiceBandRmsSpreadHz = std::sqrt(voiceBandVariance);
|
||||
float spectralFlatness = voiceBandMeanEnergy > 0.0 ? (voiceBandGeometricMean / voiceBandMeanEnergy) : 0.0f;
|
||||
float peakFraction = voiceBandTotalEnergy > 0.0 ? (voiceBandPeakEnergy / voiceBandTotalEnergy) : 1.0f;
|
||||
float peak12Ratio = voiceBandSecondPeakEnergy > 0.0 ? (voiceBandPeakEnergy / voiceBandSecondPeakEnergy) : 1000.0f;
|
||||
|
||||
int significantBins = 0;
|
||||
Real significantThreshold = voiceBandMeanEnergy * 1.8;
|
||||
|
||||
for (const Real& binEnergy : voiceBandEnergies)
|
||||
{
|
||||
if (binEnergy > significantThreshold) {
|
||||
significantBins++;
|
||||
}
|
||||
}
|
||||
|
||||
if ((peakFraction > 0.45f) || (spectralFlatness < 0.025f) || (significantBins < 5)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Additional strong-CW rejection:
|
||||
// - very narrow energy spread in voice band
|
||||
// - one dominant spectral line overwhelmingly larger than the second line
|
||||
if ((voiceBandRmsSpreadHz < 220.0f) || ((peak12Ratio > 8.0f) && (significantBins < 9))) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
float tonalPenalty = 1.0f;
|
||||
if (peakFraction > 0.30f) {
|
||||
tonalPenalty *= 0.6f;
|
||||
}
|
||||
if (spectralFlatness < 0.06f) {
|
||||
tonalPenalty *= 0.7f;
|
||||
}
|
||||
if (voiceBandRmsSpreadHz < 350.0f) {
|
||||
tonalPenalty *= 0.55f;
|
||||
}
|
||||
if (peak12Ratio > 4.0f) {
|
||||
tonalPenalty *= 0.60f;
|
||||
}
|
||||
// CW rejection end
|
||||
|
||||
// Additional F1 plausibility checks.
|
||||
// Prevent a tiny low-frequency ripple from being accepted as F1 when
|
||||
// dominant formant energy is shifted high (e.g. around 2 kHz).
|
||||
Real strongestFormantMag = 0.0;
|
||||
float strongestFormantFreq = 0.0f;
|
||||
|
||||
for (int i = 0; i < formantFreqs.size(); i++)
|
||||
{
|
||||
int idx = formantIndices[i];
|
||||
if (formantMags[idx] > strongestFormantMag)
|
||||
{
|
||||
strongestFormantMag = formantMags[idx];
|
||||
strongestFormantFreq = formantFreqs[i];
|
||||
}
|
||||
}
|
||||
|
||||
const float minF1ToStrongestRatio = 0.35f;
|
||||
const float dominantHighFormantHz = 1200.0f;
|
||||
bool weakF1 = f1Mag < strongestFormantMag * minF1ToStrongestRatio;
|
||||
bool dominantIsHigh = strongestFormantFreq >= dominantHighFormantHz;
|
||||
|
||||
if (weakF1 && dominantIsHigh) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// F1 must have a minimum contrast above noise floor.
|
||||
if (f1Mag < noiseFloor * 1.25f) {
|
||||
return 0.0;
|
||||
}
|
||||
// Additional F1 plausibility checks - end
|
||||
|
||||
// Calculate voice activity score based on formant characteristics
|
||||
// Voice is indicated by presence of F1 and F2 formants - this is the primary voice signature
|
||||
// Harmonics are less reliable in SSB due to spectral properties and noise
|
||||
float score = 0.0;
|
||||
|
||||
// Base score from number of formants (2-4 formants typical for voice)
|
||||
// Formants are the most reliable voice indicator
|
||||
// Use merged formant count, not raw peak count
|
||||
float formantScore = std::min(formantFreqs.size() / 3.0f, 1.0f);
|
||||
score += formantScore * 0.6; // 60% weight
|
||||
|
||||
// Score from formant magnitude (strong formants = strong voice)
|
||||
// Higher magnitudes indicate clearer voice detection
|
||||
// Make this threshold appropriately high to prefer strong signals
|
||||
float magnitudeScore = std::min((f1Mag + f2Mag) / (noiseFloor * 6.0f), 1.0f);
|
||||
score += magnitudeScore * 0.4; // 40% weight
|
||||
|
||||
// Apply a soft pitch-based weighting. Pitch helps detect detuning, but should not gate voice.
|
||||
const float minPitchHz = 70.0f;
|
||||
const float maxPitchHz = 300.0f;
|
||||
const float softMinPitchHz = 50.0f;
|
||||
const float softMaxPitchHz = 400.0f;
|
||||
float pitchScore = 0.7f;
|
||||
float harmonicAlignmentScore = 0.75f;
|
||||
float formantIndexScore = 0.8f;
|
||||
|
||||
if (pitchHz > 0.0f) {
|
||||
if (pitchHz >= minPitchHz && pitchHz <= maxPitchHz) {
|
||||
pitchScore = 1.0f;
|
||||
} else if (pitchHz >= softMinPitchHz && pitchHz <= softMaxPitchHz) {
|
||||
if (pitchHz < minPitchHz) {
|
||||
pitchScore = 0.7f + 0.3f * (pitchHz - softMinPitchHz) / (minPitchHz - softMinPitchHz);
|
||||
} else {
|
||||
pitchScore = 0.7f + 0.3f * (softMaxPitchHz - pitchHz) / (softMaxPitchHz - maxPitchHz);
|
||||
}
|
||||
} else {
|
||||
pitchScore = 0.6f;
|
||||
}
|
||||
|
||||
// Check harmonic-comb alignment against the estimated pitch.
|
||||
// A wrong carrier offset shifts all harmonics by a constant frequency,
|
||||
// so they no longer align with integer multiples of pitch.
|
||||
const float harmonicToleranceHz = std::max(2.0f * binBW, 0.18f * pitchHz);
|
||||
Real rawNoiseFloor = 0.0;
|
||||
int rawBinCount = 0;
|
||||
|
||||
for (int absBin = startBin; absBin <= endBin; absBin++)
|
||||
{
|
||||
float signedOffset = (absBin - carrierBin) * binBW;
|
||||
float voiceOffset = isLSB ? -signedOffset : signedOffset;
|
||||
|
||||
if (voiceOffset >= 100.0f && voiceOffset <= 3000.0f)
|
||||
{
|
||||
rawNoiseFloor += magSqFromRawFFT(absBin);
|
||||
rawBinCount++;
|
||||
}
|
||||
}
|
||||
|
||||
rawNoiseFloor = rawBinCount > 0 ? rawNoiseFloor / rawBinCount : 0.0;
|
||||
|
||||
Real alignedEnergy = 0.0;
|
||||
Real totalEnergy = 0.0;
|
||||
|
||||
for (int absBin = startBin; absBin <= endBin; absBin++)
|
||||
{
|
||||
float signedOffset = (absBin - carrierBin) * binBW;
|
||||
float voiceOffset = isLSB ? -signedOffset : signedOffset;
|
||||
|
||||
if (voiceOffset < 100.0f || voiceOffset > 3000.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Real binEnergy = magSqFromRawFFT(absBin);
|
||||
|
||||
if (binEnergy <= rawNoiseFloor * 1.2f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float residue = std::fmod(voiceOffset, pitchHz);
|
||||
if (residue < 0.0f) {
|
||||
residue += pitchHz;
|
||||
}
|
||||
|
||||
float harmonicDistance = std::min(residue, pitchHz - residue);
|
||||
Real weightedEnergy = std::max(binEnergy - rawNoiseFloor, (Real) 0.0);
|
||||
|
||||
totalEnergy += weightedEnergy;
|
||||
|
||||
if (harmonicDistance <= harmonicToleranceHz) {
|
||||
alignedEnergy += weightedEnergy;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalEnergy > 0.0)
|
||||
{
|
||||
float harmonicAlignment = alignedEnergy / totalEnergy;
|
||||
float normalizedAlignment = (harmonicAlignment - 0.20f) / 0.45f;
|
||||
normalizedAlignment = std::max(0.0f, std::min(normalizedAlignment, 1.0f));
|
||||
harmonicAlignmentScore = 0.5f + 0.5f * normalizedAlignment;
|
||||
}
|
||||
|
||||
// Check formant harmonic-index plausibility.
|
||||
// Wrong carrier tuning that shifts spectrum down can make F2/F3 appear as F1/F2.
|
||||
// In that case the implied harmonic indices become unusually high.
|
||||
float f1HarmonicIndex = formantFreqs[0] / pitchHz;
|
||||
float f2HarmonicIndex = f2Freq / pitchHz;
|
||||
float harmonicGap = f2HarmonicIndex - f1HarmonicIndex;
|
||||
|
||||
float f1IndexScore = 1.0f;
|
||||
if (f1HarmonicIndex < 2.0f) {
|
||||
f1IndexScore = std::max(0.0f, (f1HarmonicIndex - 1.0f) / 1.0f);
|
||||
} else if (f1HarmonicIndex > 18.0f) {
|
||||
f1IndexScore = std::max(0.0f, (26.0f - f1HarmonicIndex) / 8.0f);
|
||||
}
|
||||
|
||||
float f2IndexScore = 1.0f;
|
||||
if (f2HarmonicIndex < 5.0f) {
|
||||
f2IndexScore = std::max(0.0f, (f2HarmonicIndex - 3.0f) / 2.0f);
|
||||
} else if (f2HarmonicIndex > 36.0f) {
|
||||
f2IndexScore = std::max(0.0f, (44.0f - f2HarmonicIndex) / 8.0f);
|
||||
}
|
||||
|
||||
float gapScore = 1.0f;
|
||||
if (harmonicGap < 3.0f) {
|
||||
gapScore = std::max(0.0f, (harmonicGap - 1.0f) / 2.0f);
|
||||
} else if (harmonicGap > 22.0f) {
|
||||
gapScore = std::max(0.0f, (28.0f - harmonicGap) / 6.0f);
|
||||
}
|
||||
|
||||
formantIndexScore = std::max(0.4f, 0.25f + 0.75f * (0.40f * f1IndexScore + 0.40f * f2IndexScore + 0.20f * gapScore));
|
||||
}
|
||||
|
||||
score *= tonalPenalty * pitchScore * harmonicAlignmentScore * formantIndexScore;
|
||||
|
||||
// Clamp to [0, 1]
|
||||
score = std::max(0.0f, std::min(score, 1.0f));
|
||||
return score;
|
||||
}
|
||||
|
||||
// Compute formant envelope using cepstral liftering
|
||||
// This separates vocal tract resonances (formants) from pitch harmonics
|
||||
// Method: log spectrum → IFFT → lifter → FFT → exp
|
||||
void FreqScannerSink::getFormantEnvelope(int startBin, int endBin, QVector<Real>& envelope, Real *pitchHz)
|
||||
{
|
||||
if (pitchHz) {
|
||||
*pitchHz = 0.0;
|
||||
}
|
||||
|
||||
if (startBin < 0 || endBin >= m_fftSize || startBin > endBin) {
|
||||
envelope.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
int numBins = endBin - startBin + 1;
|
||||
envelope.resize(numBins);
|
||||
|
||||
// Check if cepstral FFT engines are available and large enough
|
||||
if (!m_cepstrumFFTInverse || !m_cepstrumFFTForward || numBins > m_cepstrumSize) {
|
||||
// Fallback: return simple log/exp without cepstral processing
|
||||
for (int i = 0; i < numBins; i++) {
|
||||
Real magSq = magSqFromRawFFT(startBin + i);
|
||||
envelope[i] = std::sqrt(std::max(magSq, (Real)1e-12));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 1: Compute log magnitude spectrum
|
||||
QVector<Real> logMag(numBins);
|
||||
Real minLog = -10.0; // Floor to avoid log(0)
|
||||
Real sumMagSq = 0.0;
|
||||
|
||||
for (int i = 0; i < numBins; i++)
|
||||
{
|
||||
Real magSq = magSqFromRawFFT(startBin + i);
|
||||
sumMagSq += magSq;
|
||||
Real mag = std::sqrt(std::max(magSq, (Real)1e-12));
|
||||
logMag[i] = std::log(mag);
|
||||
if (logMag[i] < minLog) {
|
||||
logMag[i] = minLog;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Apply cepstral liftering for better source-filter separation
|
||||
// Cepstral analysis separates:
|
||||
// - Voice pitch (high quefrency peak)
|
||||
// - Formants (low quefrency components)
|
||||
// Liftering = low-pass filtering in quefrency domain
|
||||
|
||||
// Copy log magnitude to inverse FFT input (real data, symmetric spectrum)
|
||||
// For real cepstrum, we need a symmetric spectrum:
|
||||
// [DC, positive freqs, Nyquist, negative freqs (mirror)]
|
||||
|
||||
// DC component
|
||||
m_cepstrumFFTInverse->in()[0] = Complex(logMag[0], 0.0);
|
||||
|
||||
// Positive frequencies
|
||||
int halfBins = std::min(numBins, m_cepstrumSize / 2);
|
||||
for (int i = 1; i < halfBins; i++) {
|
||||
m_cepstrumFFTInverse->in()[i] = Complex(logMag[i], 0.0);
|
||||
}
|
||||
|
||||
// Nyquist (if we have space)
|
||||
if (m_cepstrumSize > halfBins) {
|
||||
m_cepstrumFFTInverse->in()[halfBins] = Complex(numBins > halfBins ? logMag[halfBins] : logMag[halfBins-1], 0.0);
|
||||
}
|
||||
|
||||
// Negative frequencies (mirror of positive)
|
||||
for (int i = 1; i < halfBins; i++) {
|
||||
m_cepstrumFFTInverse->in()[m_cepstrumSize - i] = Complex(logMag[i], 0.0);
|
||||
}
|
||||
|
||||
// Zero-pad the middle if needed
|
||||
for (int i = halfBins + 1; i < m_cepstrumSize - halfBins; i++) {
|
||||
m_cepstrumFFTInverse->in()[i] = Complex(0.0, 0.0);
|
||||
}
|
||||
|
||||
// Step 3: IFFT to get cepstrum (quefrency domain)
|
||||
m_cepstrumFFTInverse->transform();
|
||||
|
||||
// Step 4: Estimate pitch from the cepstrum before liftering.
|
||||
// Pitch appears as a peak at higher quefrencies (around 3-14 ms).
|
||||
float binBW = m_scannerSampleRate / (float)m_fftSize;
|
||||
float quefrencyResolution = 1.0f / (m_cepstrumSize * binBW); // seconds per bin in quefrency
|
||||
|
||||
if (pitchHz) {
|
||||
const float minPitchHz = 70.0f;
|
||||
const float maxPitchHz = 300.0f;
|
||||
const float minQuefrency = 1.0f / maxPitchHz;
|
||||
const float maxQuefrency = 1.0f / minPitchHz;
|
||||
const int minBin = std::max(1, (int)std::ceil(minQuefrency / quefrencyResolution));
|
||||
const int maxBin = std::min(m_cepstrumSize / 2, (int)std::floor(maxQuefrency / quefrencyResolution));
|
||||
Real maxVal = 0.0;
|
||||
int maxIdx = -1;
|
||||
|
||||
for (int i = minBin; i <= maxBin; i++) {
|
||||
Real val = std::abs(m_cepstrumFFTInverse->out()[i].real());
|
||||
if (val > maxVal) {
|
||||
maxVal = val;
|
||||
maxIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxIdx > 0) {
|
||||
*pitchHz = 1.0f / (maxIdx * quefrencyResolution);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Apply lifter (low-pass filter in quefrency domain)
|
||||
// Lifter cutoff: keep low quefrencies (formant envelope), remove high quefrencies (pitch harmonics)
|
||||
// Typical pitch periods: 3-10 ms (100-330 Hz F0)
|
||||
// We want to remove quefrencies corresponding to pitch harmonics
|
||||
|
||||
// Lifter cutoff in seconds (quefrency): keep components below this
|
||||
// Use much lower cutoff - we only need to keep very low quefrencies for formant envelope
|
||||
// Most formant information is in the first few quefrency bins
|
||||
float lifterCutoffQuefrency = 0.002f; // 2 ms (reduced from 8 ms)
|
||||
int lifterCutoffBin = std::max(1, (int)(lifterCutoffQuefrency / quefrencyResolution));
|
||||
|
||||
// Cap the lifter cutoff to reasonable maximum (1/4 of cepstrum size)
|
||||
lifterCutoffBin = std::min(lifterCutoffBin, m_cepstrumSize / 4);
|
||||
|
||||
// Apply lifter: keep low quefrencies, zero out high quefrencies
|
||||
// Use smooth transition (raised cosine) to reduce artifacts
|
||||
int transitionBins = std::max(1, lifterCutoffBin / 4);
|
||||
|
||||
for (int i = 0; i < m_cepstrumSize; i++) {
|
||||
Real lifterWeight = 1.0;
|
||||
|
||||
if (i > lifterCutoffBin + transitionBins) {
|
||||
lifterWeight = 0.0; // Zero out high quefrencies
|
||||
} else if (i > lifterCutoffBin) {
|
||||
// Smooth transition using raised cosine
|
||||
float t = (float)(i - lifterCutoffBin) / transitionBins;
|
||||
lifterWeight = 0.5 * (1.0 + std::cos(M_PI * t));
|
||||
}
|
||||
// else: lifterWeight = 1.0 (keep low quefrencies)
|
||||
|
||||
m_cepstrumFFTForward->in()[i] = m_cepstrumFFTInverse->out()[i] * lifterWeight;
|
||||
}
|
||||
|
||||
// Step 6: FFT back to frequency domain (smoothed log spectrum)
|
||||
m_cepstrumFFTForward->transform();
|
||||
|
||||
// Step 7: Convert back to linear magnitude
|
||||
// Take real part of FFT output and exponentiate
|
||||
// IMPORTANT: Normalize by FFT size since FFT engines don't auto-normalize
|
||||
Real normalization = 1.0 / m_cepstrumSize;
|
||||
|
||||
for (int i = 0; i < numBins; i++)
|
||||
{
|
||||
Real smoothedLog = m_cepstrumFFTForward->out()[i].real() * normalization;
|
||||
envelope[i] = std::exp(smoothedLog);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,12 +74,24 @@ private:
|
||||
FixedAverage2D<Real> m_fftAverage; // magSq average
|
||||
QVector<Real> m_magSq;
|
||||
int m_averageCount;
|
||||
QVector<Real> m_voiceLevelSum; // Sum of voice levels for averaging
|
||||
QVector<int> m_voiceLevelCount; // Count of voice level samples
|
||||
|
||||
// Cepstral analysis FFT engines for formant detection
|
||||
int m_cepstrumSequenceInverse;
|
||||
int m_cepstrumSequenceForward;
|
||||
FFTEngine *m_cepstrumFFTInverse;
|
||||
FFTEngine *m_cepstrumFFTForward;
|
||||
int m_cepstrumSize;
|
||||
|
||||
void processOneSample(Complex &ci);
|
||||
MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; }
|
||||
Real totalPower(int bin, int channelBins) const;
|
||||
Real peakPower(int bin, int channelBins) const;
|
||||
Real magSq(int bin) const;
|
||||
Real magSqFromRawFFT(int bin) const;
|
||||
Real voiceActivityLevel(int bin, int channelBins, bool isLSB);
|
||||
void getFormantEnvelope(int startBin, int endBin, QVector<Real>& envelope, Real *pitchHz = nullptr);
|
||||
};
|
||||
|
||||
#endif // INCLUDE_FREQSCANNERSINK_H
|
||||
|
||||
@@ -30,47 +30,51 @@ Use the wheels of keyboard to adjust the minimum frequency shift in Hz from the
|
||||
|
||||
This setting is typically used to avoid having the channel (1) centered at DC, which can be problematic for some demodulators used with SDRs with a DC spike.
|
||||
|
||||
<h3>3: Active frequency power</h3>
|
||||
<h3>3: Lock device frequency</h3>
|
||||
|
||||
Locks device frequency to its current value. This is useful for network devices like the TCP input that has a very long latency for setting the frequency. It makes impractical to move the device frequency. In this kind of setup you have to make sure that the scanned channels all fit in the same baseband.
|
||||
|
||||
<h3>4: Active frequency power</h3>
|
||||
|
||||
Average power in dB relative to a +/- 1.0 amplitude signal received for the active frequency. This is set to '-' while scanning.
|
||||
|
||||
<h3>4: TH - Threshold</h3>
|
||||
<h3>5: TH - Threshold</h3>
|
||||
|
||||
Power threshold in dB that determines whether a frequency is active or not.
|
||||
|
||||
<h3>5: t_delta_f - Tune time</h3>
|
||||
<h3>6: t_delta_f - Tune time</h3>
|
||||
|
||||
Specifies the time in milliseconds that the Frequency Scanner should wait after adjusting the device center frequency, before starting a measurement.
|
||||
This time should take in to account PLL settle time and the device to host transfer latency, so that the measurement only starts when IQ data
|
||||
that corresponds to the set frequency is being received.
|
||||
|
||||
<h3>6: t_s - Scan time</h3>
|
||||
<h3>7: t_s - Scan time</h3>
|
||||
|
||||
Specifies the time in seconds that the Frequency Scanner will average its power measurement over.
|
||||
|
||||
<h3>7: t_rtx - Retransmission Time / t_rx Receive Time</h3>
|
||||
<h3>8: t_rtx - Retransmission Time / t_rx Receive Time</h3>
|
||||
|
||||
t_rtx: When Run Mode (11) is not Multiplex, specifies the time in seconds that the Frequency Scanner will wait after the power on the active frequency falls below the threshold, before restarting
|
||||
scanning. This enables the channel to remain tuned to a single frequency while there is a temporary break in transmission.
|
||||
|
||||
t_rx: When Run Mode (11) is Multiplex, specifies the time in seconds the channel will be tuned to each frequency.
|
||||
|
||||
<h3>8: Ch BW - Channel Bandwidth</h3>
|
||||
<h3>9: Ch BW - Channel Bandwidth</h3>
|
||||
|
||||
This specifies the bandwidth of the channels to be scanned.
|
||||
|
||||
<h3>9: Channel shift</h3>
|
||||
<h3>10: Channel shift</h3>
|
||||
|
||||
This shift is applied to the controlled channel from the scanned center frequency. This is useful for SSB or CW or generally whenever the signal of interest is only on one side of the controlled channel center frequency.
|
||||
|
||||
<h3>10: Pri - Priority</h3>
|
||||
<h3>11: Pri - Priority</h3>
|
||||
|
||||
Specifies which frequency will be chosen as the active frequency, when multiple frequencies exceed the threshold (4):
|
||||
|
||||
- Max power: The frequency with the highest power will be chosen
|
||||
- Max power: The frequency with the highest power will be chosen. If VAD is active this is the highest voice score/
|
||||
- Table order: The frequency first in the frequency table (14) will be chosen.
|
||||
|
||||
<h3>11: Meas - Power Measurement</h3>
|
||||
<h3>12: Meas - Power Measurement</h3>
|
||||
|
||||
Specifies how power is measured. In both cases, a FFT is used.
|
||||
FFT size is typically the same as used for the Main Spectrum, but may be increased to ensure at least 8 bins cover the channel bandwidth (8).
|
||||
@@ -82,7 +86,7 @@ The first and last bins are excluded from the measurement (to reduce spectral le
|
||||
Peak can be used when you wish to set the threshold roughly according to the level displayed in the Main Spectrum.
|
||||
Total is potentially more useful for wideband signals, that are close to the noise floor.
|
||||
|
||||
<h3>12: Run Mode</h3>
|
||||
<h3>13: Run Mode</h3>
|
||||
|
||||
Specifies the run mode:
|
||||
|
||||
@@ -91,18 +95,38 @@ Specifies the run mode:
|
||||
- Scan only: All frequencies are scanned repeatedly. The channel will not be tuned. This mode is just for counting how often frequencies are active, which can be seen in the Active Count column in the frequency table (14).
|
||||
- Multiplex: Frequencies will be stepped through sequentially and repeatedly, with the channel (1) being tuned for the time specified by t_rx (7).
|
||||
|
||||
<h3>13: Start/Stop Scanning</h3>
|
||||
<h3>14: Start/Stop Scanning</h3>
|
||||
|
||||
Press this button to start or stop scanning.
|
||||
|
||||
<h3>14: Status Text</h3>
|
||||
<h3>15: Restart scanning</h3>
|
||||
|
||||
Forces resuming scanning.
|
||||
|
||||
<h3>16: Status Text</h3>
|
||||
|
||||
Displays the current status of the Frequency Scanner.
|
||||
|
||||
- "Scanning": When scanning for active frequencies.
|
||||
- Frequency and annotation for active frequency.
|
||||
|
||||
<h3>15: Frequency Table</h3>
|
||||
<h3>17: Voice Activity Detection (VAD) mode</h3>
|
||||
|
||||
- **None**: No VAD is active. Scanning is based on channel power only
|
||||
- **LSB**: For SSB LSB, scanning is based on the likelihood of the audio to be a human voice. This yields a score between 0.0 and 1.0 that can be used to control the scanner (18)
|
||||
- **USB**: Same thing but for SSB USB mode
|
||||
|
||||
SSB voice detection makes the following assumptions:
|
||||
- the channel bandwidth (9) is set to 3000 Hz and the shift (10) to 1500 Hz for LSB and -1500 Hz for USB so that the audio spectrum from 0 to 3000 Hz fits in the passband.
|
||||
- as most of the time transmissions occur on integer multiples of the kHz you must set your scanning frequencies as a comb of frequencies 1 kHz apart on 500 Hz points (e.g 7110.5, 7111.5, ... kHz)
|
||||
- the VAD threshold value (18) works best for values in the 0.75~0.85 range.
|
||||
- of course you must make sure that the controlled channel supports SSB and is set to the correct sideband mode.
|
||||
|
||||
<h3>18: Voice Activity threshold</h3>
|
||||
|
||||
In LSB and USB VAD modes, voice likelihood score above which a channel is declared active.
|
||||
|
||||
<h3> Frequency Table</h3>
|
||||
|
||||
The frequency table contains the list of frequencies to be scanned, along with results of a scan. The columns are:
|
||||
|
||||
@@ -110,6 +134,7 @@ The frequency table contains the list of frequencies to be scanned, along with r
|
||||
- Annotation: An annotation (description) for the frequency, that is obtained from the closest matching [annotation marker](../../../sdrgui/gui/spectrummarkers.md) in the Main Spectrum.
|
||||
- Enable: Determines whether the frequency will be scanned. This can be used to temporarily disable frequencies you aren't interested in.
|
||||
- Power (dB): Displays the measured power in decibels from the last scan. The cell will have a green background if the power was above the threshold (4).
|
||||
- VAD: Voice activity likelihood score in the [0.0, 1.0] range
|
||||
- Active Count: Displays the number of scans in which the power for this frequency was above the threshold (4). This allows you to see which frequencies are commonly in use.
|
||||
- Notes: Available for user-entry of notes/information about this frequency.
|
||||
- Channel: Specifies the channel that should be tuned when this frequency is active. If blank, the common Channel setting (1) is used.
|
||||
@@ -127,32 +152,36 @@ Right clicking on a cell will display a popup menu:
|
||||
- Remove selected rows.
|
||||
- Tune selected channel (1) to the frequency in the row clicked on.
|
||||
|
||||
<h3>16: Add</h3>
|
||||
<h3>20: Add</h3>
|
||||
|
||||
Press to add a single row to the frequency table (14).
|
||||
|
||||
<h3>17: Add Range</h3>
|
||||
<h3>21: Add Range</h3>
|
||||
|
||||
Press to add a range of frequencies to the frequency table (14). A dialog is displayed with start and stop frequencies, as well as a step value.
|
||||
Press to add a range of frequencies to the frequency table. A dialog is displayed with start and stop frequencies, as well as a step value.
|
||||
The step value should typically be an integer multiple of the channel bandwidth (8).
|
||||
|
||||
<h3>18: Remove</h3>
|
||||
<h3>22: Remove</h3>
|
||||
|
||||
Removes the selected rows from the frequency table (14). Press Ctrl-A to select all rows.
|
||||
|
||||
<h3>19: Remove Inactive</h3>
|
||||
<h3>23: Remove Inactive</h3>
|
||||
|
||||
Removes all rows with Active Count of 0.
|
||||
|
||||
<h3>20: Up</h3>
|
||||
<h3>24: Remove all</h3>
|
||||
|
||||
Remove all rows unconditionnally
|
||||
|
||||
<h3>25: Up</h3>
|
||||
|
||||
Moves the selected rows up the frequency table (14).
|
||||
|
||||
<h3>21: Down</h3>
|
||||
<h3>26: Down</h3>
|
||||
|
||||
Moves the selected rows the the frequency table (14).
|
||||
|
||||
<h3>22: Import Frequencies from .csv</h3>
|
||||
<h3>27: Import Frequencies from .csv</h3>
|
||||
|
||||
Imports frequencies from a .csv file.
|
||||
|
||||
@@ -160,11 +189,11 @@ The expected column names are "Freq (Hz)", "Enable", "Notes", "Channel", "Ch BW
|
||||
|
||||
Annotations are not included. These should be imported via the Spectrum Markers dialog.
|
||||
|
||||
<h3>23: Export Frequencies to .csv</h3>
|
||||
<h3>28: Export Frequencies to .csv</h3>
|
||||
|
||||
Exports frequencies to a .csv file. Note that annotations are not included. These should be exported via the Spectrum Markers dialog.
|
||||
|
||||
<h3>24: Clear Active Count</h3>
|
||||
<h3>29: Clear Active Count</h3>
|
||||
|
||||
Press to reset the value in the Active Count column to 0 for all rows.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user