1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-04-25 00:43:59 -04:00

Freq Scanner voice squelch: compute formants on individual FFTs

This commit is contained in:
f4exb 2026-02-15 21:28:05 +01:00
parent 37afa7be74
commit 65e999f842
2 changed files with 93 additions and 15 deletions

View File

@ -95,6 +95,51 @@ 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;
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++) {
@ -147,12 +192,12 @@ void FreqScannerSink::processOneSample(Complex &ci)
power = totalPower(bin, channelBins);
}
// Calculate voice activity level if using voice trigger
// Use averaged voice activity level from individual FFTs
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 ((m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceLsb ||
m_settings.m_voiceSquelchType == FreqScannerSettings::VoiceUsb) &&
m_voiceLevelCount[i] > 0) {
voiceLevel = m_voiceLevelSum[i] / m_voiceLevelCount[i];
}
//qDebug() << "startFrequency:" << startFrequency << "m_scannerSampleRate:" << m_scannerSampleRate << "m_centerFrequency:" << m_centerFrequency << "frequency" << frequency << "bin" << bin << "power" << power << "voiceLevel" << voiceLevel;
@ -165,6 +210,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;
}
@ -216,6 +265,23 @@ 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:"
@ -252,6 +318,13 @@ 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);
}
m_channelSampleRate = channelSampleRate;
@ -307,35 +380,37 @@ Real FreqScannerSink::voiceActivityLevel(int bin, int channelBins, bool isLSB) c
QVector<int> peakBins;
QVector<Real> peakMags;
// Calculate average noise floor
// Calculate average noise floor from raw FFT
Real noiseFloor = 0.0;
int noiseCount = 0;
for (int i = startBin; i <= endBin; i++) {
noiseFloor += m_magSq[i];
noiseFloor += magSqFromRawFFT(i);
noiseCount++;
}
noiseFloor = (noiseCount > 0) ? (noiseFloor / noiseCount) : 1e-12;
Real threshold = noiseFloor * 3.0; // 4.77 dB above noise
// Simple peak detection
// Simple peak detection using raw FFT data
int i = searchStart;
while ((isLSB && i >= searchEnd) || (!isLSB && i <= searchEnd))
{
if (m_magSq[i] > threshold)
Real binMagSq = magSqFromRawFFT(i);
if (binMagSq > threshold)
{
// Found potential peak start
Real peakMag = m_magSq[i];
Real peakMag = binMagSq;
int peakBin = i;
// Find local maximum
i += step;
while ((isLSB && i >= searchEnd) || (!isLSB && i <= searchEnd))
{
if (m_magSq[i] > peakMag) {
peakMag = m_magSq[i];
Real nextMagSq = magSqFromRawFFT(i);
if (nextMagSq > peakMag) {
peakMag = nextMagSq;
peakBin = i;
i += step;
} else if (m_magSq[i] > threshold) {
} else if (nextMagSq > threshold) {
i += step;
} else {
break; // Peak ended
@ -374,7 +449,7 @@ Real FreqScannerSink::voiceActivityLevel(int bin, int channelBins, bool isLSB) c
// Search left
for (int j = peakBin - 1; j >= startBin; j--) {
if (m_magSq[j] > halfPower) {
if (magSqFromRawFFT(j) > halfPower) {
bwCount++;
} else {
break;
@ -383,7 +458,7 @@ Real FreqScannerSink::voiceActivityLevel(int bin, int channelBins, bool isLSB) c
// Search right
for (int j = peakBin + 1; j <= endBin; j++) {
if (m_magSq[j] > halfPower) {
if (magSqFromRawFFT(j) > halfPower) {
bwCount++;
} else {
break;

View File

@ -74,12 +74,15 @@ 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
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) const;
};