1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-02-17 07:03:41 -05:00

Experimental NFM demod

This commit is contained in:
f4exb 2019-11-23 07:48:25 +01:00
parent 60927fc4ac
commit 6944dd7013
19 changed files with 3449 additions and 0 deletions

View File

@ -0,0 +1,62 @@
project(nfmtest)
set(nfmtest_SOURCES
nfmtestdemod.cpp
nfmtestdemodsettings.cpp
nfmtestdemodwebapiadapter.cpp
nfmtestplugin.cpp
nfmdemodreport.cpp
nfmdemodsink.cpp
nfmdemodbaseband.cpp
)
set(nfmtest_HEADERS
nfmtestdemod.h
nfmtestdemodsettings.h
nfmtestdemodwebapiadapter.h
nfmtestplugin.h
nfmdemodreport.h
nfmdemodsink.h
nfmdemodbaseband.h
)
include_directories(
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
)
if(NOT SERVER_MODE)
set(nfmtest_SOURCES
${nfmtest_SOURCES}
nfmtestdemodgui.cpp
nfmtestdemodgui.ui
)
set(nfmtest_HEADERS
${nfmtest_HEADERS}
nfmtestdemodgui.h
)
set(TARGET_NAME demodnfmtest)
set(TARGET_LIB "Qt5::Widgets")
set(TARGET_LIB_GUI "sdrgui")
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
else()
set(TARGET_NAME demodnfmtestsrv)
set(TARGET_LIB "")
set(TARGET_LIB_GUI "")
set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR})
endif()
add_library(${TARGET_NAME} SHARED
${nfmtest_SOURCES}
)
target_link_libraries(${TARGET_NAME}
Qt5::Core
${TARGET_LIB}
sdrbase
${TARGET_LIB_GUI}
swagger
)
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})

View File

@ -0,0 +1,170 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "dsp/downsamplechannelizer.h"
#include "nfmdemodbaseband.h"
MESSAGE_CLASS_DEFINITION(NFMDemodBaseband::MsgConfigureNFMDemodBaseband, Message)
NFMDemodBaseband::NFMDemodBaseband() :
m_mutex(QMutex::Recursive)
{
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000));
m_channelizer = new DownSampleChannelizer(&m_sink);
qDebug("NFMDemodBaseband::NFMDemodBaseband");
QObject::connect(
&m_sampleFifo,
&SampleSinkFifo::dataReady,
this,
&NFMDemodBaseband::handleData,
Qt::QueuedConnection
);
DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(m_sink.getAudioFifo(), getInputMessageQueue());
m_sink.applyAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getOutputSampleRate());
connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
}
NFMDemodBaseband::~NFMDemodBaseband()
{
DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(m_sink.getAudioFifo());
delete m_channelizer;
}
void NFMDemodBaseband::reset()
{
QMutexLocker mutexLocker(&m_mutex);
m_sampleFifo.reset();
}
void NFMDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end)
{
m_sampleFifo.write(begin, end);
}
void NFMDemodBaseband::handleData()
{
while ((m_sampleFifo.fill() > 0) && (m_inputMessageQueue.size() == 0))
{
SampleVector::iterator part1begin;
SampleVector::iterator part1end;
SampleVector::iterator part2begin;
SampleVector::iterator part2end;
std::size_t count = m_sampleFifo.readBegin(m_sampleFifo.fill(), &part1begin, &part1end, &part2begin, &part2end);
// first part of FIFO data
if (part1begin != part1end) {
m_channelizer->feed(part1begin, part1end);
}
// second part of FIFO data (used when block wraps around)
if(part2begin != part2end) {
m_channelizer->feed(part2begin, part2end);
}
m_sampleFifo.readCommit((unsigned int) count);
}
}
void NFMDemodBaseband::handleInputMessages()
{
Message* message;
while ((message = m_inputMessageQueue.pop()) != nullptr)
{
if (handleMessage(*message)) {
delete message;
}
}
}
bool NFMDemodBaseband::handleMessage(const Message& cmd)
{
if (MsgConfigureNFMDemodBaseband::match(cmd))
{
QMutexLocker mutexLocker(&m_mutex);
MsgConfigureNFMDemodBaseband& cfg = (MsgConfigureNFMDemodBaseband&) cmd;
qDebug() << "NFMDemodBaseband::handleMessage: MsgConfigureNFMDemodBaseband";
applySettings(cfg.getSettings(), cfg.getForce());
return true;
}
else if (DSPSignalNotification::match(cmd))
{
QMutexLocker mutexLocker(&m_mutex);
DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
qDebug() << "NFMDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate();
m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate()));
m_channelizer->setBasebandSampleRate(notif.getSampleRate());
m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset());
return true;
}
else
{
return false;
}
}
void NFMDemodBaseband::applySettings(const NFMDemodTestSettings& settings, bool force)
{
if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force)
{
m_channelizer->setChannelization(m_sink.getAudioSampleRate(), settings.m_inputFrequencyOffset);
m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset());
}
if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force)
{
AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager();
int audioDeviceIndex = audioDeviceManager->getOutputDeviceIndex(settings.m_audioDeviceName);
//qDebug("AMDemod::applySettings: audioDeviceName: %s audioDeviceIndex: %d", qPrintable(settings.m_audioDeviceName), audioDeviceIndex);
audioDeviceManager->addAudioSink(m_sink.getAudioFifo(), getInputMessageQueue(), audioDeviceIndex);
uint32_t audioSampleRate = audioDeviceManager->getOutputSampleRate(audioDeviceIndex);
if (m_sink.getAudioSampleRate() != audioSampleRate)
{
m_sink.applyAudioSampleRate(audioSampleRate);
m_channelizer->setChannelization(audioSampleRate, settings.m_inputFrequencyOffset);
m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset());
}
}
m_sink.applySettings(settings, force);
m_settings = settings;
}
int NFMDemodBaseband::getChannelSampleRate() const
{
return m_channelizer->getChannelSampleRate();
}
void NFMDemodBaseband::setBasebandSampleRate(int sampleRate)
{
m_channelizer->setBasebandSampleRate(sampleRate);
}

View File

@ -0,0 +1,89 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_NFMDEMODBASEBAND_H
#define INCLUDE_NFMDEMODBASEBAND_H
#include <QObject>
#include <QMutex>
#include "dsp/samplesinkfifo.h"
#include "util/message.h"
#include "util/messagequeue.h"
#include "nfmdemodsink.h"
class DownSampleChannelizer;
class NFMDemodBaseband : public QObject
{
Q_OBJECT
public:
class MsgConfigureNFMDemodBaseband : public Message {
MESSAGE_CLASS_DECLARATION
public:
const NFMDemodTestSettings& getSettings() const { return m_settings; }
bool getForce() const { return m_force; }
static MsgConfigureNFMDemodBaseband* create(const NFMDemodTestSettings& settings, bool force)
{
return new MsgConfigureNFMDemodBaseband(settings, force);
}
private:
NFMDemodTestSettings m_settings;
bool m_force;
MsgConfigureNFMDemodBaseband(const NFMDemodTestSettings& settings, bool force) :
Message(),
m_settings(settings),
m_force(force)
{ }
};
NFMDemodBaseband();
~NFMDemodBaseband();
void reset();
void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end);
MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication
int getChannelSampleRate() const;
void getMagSqLevels(double& avg, double& peak, int& nbSamples) { m_sink.getMagSqLevels(avg, peak, nbSamples); }
void setSelectedCtcssIndex(int selectedCtcssIndex) { m_sink.setSelectedCtcssIndex(selectedCtcssIndex); }
bool getSquelchOpen() const { return m_sink.getSquelchOpen(); }
const Real *getCtcssToneSet(int& nbTones) const { return m_sink.getCtcssToneSet(nbTones); }
void setMessageQueueToGUI(MessageQueue *messageQueue) { m_sink.setMessageQueueToGUI(messageQueue); }
unsigned int getAudioSampleRate() const { return m_sink.getAudioSampleRate(); }
void setBasebandSampleRate(int sampleRate);
private:
SampleSinkFifo m_sampleFifo;
DownSampleChannelizer *m_channelizer;
NFMDemodSink m_sink;
MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication
NFMDemodTestSettings m_settings;
QMutex m_mutex;
bool handleMessage(const Message& cmd);
void applySettings(const NFMDemodTestSettings& settings, bool force = false);
private slots:
void handleInputMessages();
void handleData(); //!< Handle data when samples have to be processed
};
#endif // INCLUDE_NFMDEMODBASEBAND_H

View File

@ -0,0 +1,26 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "nfmdemodreport.h"
MESSAGE_CLASS_DEFINITION(NFMDemodReport::MsgReportCTCSSFreq, Message)
NFMDemodReport::NFMDemodReport()
{ }
NFMDemodReport::~NFMDemodReport()
{ }

View File

@ -0,0 +1,55 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef PLUGINS_CHANNELRX_DEMONFM_NFMDEMODREPORT_H_
#define PLUGINS_CHANNELRX_DEMONFM_NFMDEMODREPORT_H_
#include <QObject>
#include "dsp/dsptypes.h"
#include "util/message.h"
class NFMDemodReport : public QObject
{
Q_OBJECT
public:
class MsgReportCTCSSFreq : public Message {
MESSAGE_CLASS_DECLARATION
public:
Real getFrequency() const { return m_freq; }
static MsgReportCTCSSFreq* create(Real freq)
{
return new MsgReportCTCSSFreq(freq);
}
private:
Real m_freq;
MsgReportCTCSSFreq(Real freq) :
Message(),
m_freq(freq)
{ }
};
public:
NFMDemodReport();
~NFMDemodReport();
};
#endif // PLUGINS_CHANNELRX_DEMONFM_NFMDEMODREPORT_H_

View File

@ -0,0 +1,385 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <complex.h>
#include <QTime>
#include <QDebug>
#include "util/stepfunctions.h"
#include "util/db.h"
#include "audio/audiooutput.h"
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "dsp/devicesamplemimo.h"
#include "device/deviceapi.h"
#include "nfmdemodreport.h"
#include "nfmdemodsink.h"
const double NFMDemodSink::afSqTones[] = {1000.0, 6000.0}; // {1200.0, 8000.0};
const double NFMDemodSink::afSqTones_lowrate[] = {1000.0, 3500.0};
NFMDemodSink::NFMDemodSink() :
m_channelSampleRate(48000),
m_channelFrequencyOffset(0),
m_audioSampleRate(48000),
m_audioBufferFill(0),
m_audioFifo(48000),
m_ctcssIndex(0),
m_sampleCount(0),
m_squelchCount(0),
m_squelchGate(4800),
m_squelchLevel(-990),
m_squelchOpen(false),
m_afSquelchOpen(false),
m_magsq(0.0f),
m_magsqSum(0.0f),
m_magsqPeak(0.0f),
m_magsqCount(0),
m_afSquelch(),
m_squelchDelayLine(24000),
m_messageQueueToGUI(nullptr)
{
m_agcLevel = 1.0;
m_audioBuffer.resize(1<<14);
applySettings(m_settings, true);
applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true);
}
NFMDemodSink::~NFMDemodSink()
{
}
void NFMDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end)
{
Complex ci;
for (SampleVector::const_iterator it = begin; it != end; ++it)
{
Complex c(it->real(), it->imag());
c *= m_nco.nextIQ();
if (m_interpolatorDistance < 1.0f) // interpolate
{
while (!m_interpolator.interpolate(&m_interpolatorDistanceRemain, c, &ci))
{
processOneSample(ci);
m_interpolatorDistanceRemain += m_interpolatorDistance;
}
}
else // decimate
{
if (m_interpolator.decimate(&m_interpolatorDistanceRemain, c, &ci))
{
processOneSample(ci);
m_interpolatorDistanceRemain += m_interpolatorDistance;
}
}
}
}
void NFMDemodSink::processOneSample(Complex &ci)
{
qint16 sample;
double magsqRaw; // = ci.real()*ci.real() + c.imag()*c.imag();
Real deviation;
Real demod = m_phaseDiscri.phaseDiscriminatorDelta(ci, magsqRaw, deviation);
Real magsq = magsqRaw / (SDR_RX_SCALED*SDR_RX_SCALED);
m_movingAverage(magsq);
m_magsqSum += magsq;
if (magsq > m_magsqPeak)
{
m_magsqPeak = magsq;
}
m_magsqCount++;
m_sampleCount++;
// AF processing
if (m_settings.m_deltaSquelch)
{
if (m_afSquelch.analyze(demod * m_discriCompensation))
{
m_afSquelchOpen = m_afSquelch.evaluate(); // ? m_squelchGate + m_squelchDecay : 0;
if (!m_afSquelchOpen) {
m_squelchDelayLine.zeroBack(m_audioSampleRate/10); // zero out evaluation period
}
}
if (m_afSquelchOpen)
{
m_squelchDelayLine.write(demod * m_discriCompensation);
if (m_squelchCount < 2*m_squelchGate) {
m_squelchCount++;
}
}
else
{
m_squelchDelayLine.write(0);
if (m_squelchCount > 0) {
m_squelchCount--;
}
}
}
else
{
if ((Real) m_movingAverage < m_squelchLevel)
{
m_squelchDelayLine.write(0);
if (m_squelchCount > 0) {
m_squelchCount--;
}
}
else
{
m_squelchDelayLine.write(demod * m_discriCompensation);
if (m_squelchCount < 2*m_squelchGate) {
m_squelchCount++;
}
}
}
m_squelchOpen = (m_squelchCount > m_squelchGate);
if (m_settings.m_audioMute)
{
sample = 0;
}
else
{
if (m_squelchOpen)
{
if (m_settings.m_ctcssOn)
{
Real ctcss_sample = m_ctcssLowpass.filter(demod * m_discriCompensation);
if ((m_sampleCount & 7) == 7) // decimate 48k -> 6k
{
if (m_ctcssDetector.analyze(&ctcss_sample))
{
int maxToneIndex;
if (m_ctcssDetector.getDetectedTone(maxToneIndex))
{
if (maxToneIndex+1 != m_ctcssIndex)
{
if (getMessageQueueToGUI())
{
NFMDemodReport::MsgReportCTCSSFreq *msg = NFMDemodReport::MsgReportCTCSSFreq::create(m_ctcssDetector.getToneSet()[maxToneIndex]);
getMessageQueueToGUI()->push(msg);
}
m_ctcssIndex = maxToneIndex+1;
}
}
else
{
if (m_ctcssIndex != 0)
{
if (getMessageQueueToGUI())
{
NFMDemodReport::MsgReportCTCSSFreq *msg = NFMDemodReport::MsgReportCTCSSFreq::create(0);
getMessageQueueToGUI()->push(msg);
}
m_ctcssIndex = 0;
}
}
}
}
}
if (m_settings.m_ctcssOn && m_ctcssIndexSelected && (m_ctcssIndexSelected != m_ctcssIndex))
{
sample = 0;
}
else
{
if (m_settings.m_highPass) {
sample = m_bandpass.filter(m_squelchDelayLine.readBack(m_squelchGate)) * m_settings.m_volume;
} else {
sample = m_lowpass.filter(m_squelchDelayLine.readBack(m_squelchGate)) * m_settings.m_volume * 301.0f;
}
}
}
else
{
if (m_ctcssIndex != 0)
{
if (getMessageQueueToGUI())
{
NFMDemodReport::MsgReportCTCSSFreq *msg = NFMDemodReport::MsgReportCTCSSFreq::create(0);
getMessageQueueToGUI()->push(msg);
}
m_ctcssIndex = 0;
}
sample = 0;
}
}
m_audioBuffer[m_audioBufferFill].l = sample;
m_audioBuffer[m_audioBufferFill].r = sample;
++m_audioBufferFill;
if (m_audioBufferFill >= m_audioBuffer.size())
{
uint res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill);
if (res != m_audioBufferFill)
{
qDebug("NFMDemodSink::feed: %u/%u audio samples written", res, m_audioBufferFill);
qDebug("NFMDemodSink::feed: m_audioSampleRate: %u m_channelSampleRate: %d", m_audioSampleRate, m_channelSampleRate);
}
m_audioBufferFill = 0;
}
}
void NFMDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force)
{
qDebug() << "NFMDemodSink::applyChannelSettings:"
<< " channelSampleRate: " << channelSampleRate
<< " channelFrequencyOffset: " << channelFrequencyOffset;
if ((channelFrequencyOffset != m_channelFrequencyOffset) ||
(channelSampleRate != m_channelSampleRate) || force)
{
m_nco.setFreq(-channelFrequencyOffset, channelSampleRate);
}
if ((channelSampleRate != m_channelSampleRate) || force)
{
m_interpolator.create(16, channelSampleRate, m_settings.m_rfBandwidth / 2.2);
m_interpolatorDistanceRemain = 0;
m_interpolatorDistance = (Real) channelSampleRate / (Real) m_audioSampleRate;
}
m_channelSampleRate = channelSampleRate;
m_channelFrequencyOffset = channelFrequencyOffset;
}
void NFMDemodSink::applySettings(const NFMDemodTestSettings& settings, bool force)
{
qDebug() << "NFMDemodSink::applySettings:"
<< " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset
<< " m_rfBandwidth: " << settings.m_rfBandwidth
<< " m_afBandwidth: " << settings.m_afBandwidth
<< " m_fmDeviation: " << settings.m_fmDeviation
<< " m_volume: " << settings.m_volume
<< " m_squelchGate: " << settings.m_squelchGate
<< " m_deltaSquelch: " << settings.m_deltaSquelch
<< " m_squelch: " << settings.m_squelch
<< " m_ctcssIndex: " << settings.m_ctcssIndex
<< " m_ctcssOn: " << settings.m_ctcssOn
<< " m_highPass: " << settings.m_highPass
<< " m_audioMute: " << settings.m_audioMute
<< " m_audioDeviceName: " << settings.m_audioDeviceName
<< " force: " << force;
if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force)
{
m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2);
m_interpolatorDistanceRemain = 0;
m_interpolatorDistance = (Real) m_channelSampleRate / (Real) m_audioSampleRate;
}
if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force)
{
m_phaseDiscri.setFMScaling((8.0f*m_audioSampleRate) / static_cast<float>(settings.m_fmDeviation)); // integrate 4x factor
}
if ((settings.m_afBandwidth != m_settings.m_afBandwidth) || force)
{
m_bandpass.create(301, m_audioSampleRate, 300.0, settings.m_afBandwidth);
m_lowpass.create(301, m_audioSampleRate, settings.m_afBandwidth);
}
if ((settings.m_squelchGate != m_settings.m_squelchGate) || force)
{
m_squelchGate = (m_audioSampleRate / 100) * settings.m_squelchGate; // gate is given in 10s of ms at 48000 Hz audio sample rate
m_squelchCount = 0; // reset squelch open counter
}
if ((settings.m_squelch != m_settings.m_squelch) ||
(settings.m_deltaSquelch != m_settings.m_deltaSquelch) || force)
{
if (settings.m_deltaSquelch)
{ // input is a value in negative centis
m_squelchLevel = (- settings.m_squelch) / 100.0;
m_afSquelch.setThreshold(m_squelchLevel);
m_afSquelch.reset();
}
else
{ // input is a value in deci-Bels
m_squelchLevel = std::pow(10.0, settings.m_squelch / 10.0);
m_movingAverage.reset();
}
m_squelchCount = 0; // reset squelch open counter
}
if ((settings.m_ctcssIndex != m_settings.m_ctcssIndex) || force) {
setSelectedCtcssIndex(settings.m_ctcssIndex);
}
m_settings = settings;
}
void NFMDemodSink::applyAudioSampleRate(unsigned int sampleRate)
{
qDebug("NFMDemodSink::applyAudioSampleRate: %u m_channelSampleRate: %d", sampleRate, m_channelSampleRate);
m_ctcssLowpass.create(301, sampleRate, 250.0);
m_bandpass.create(301, sampleRate, 300.0, m_settings.m_afBandwidth);
m_lowpass.create(301, sampleRate, m_settings.m_afBandwidth);
m_squelchGate = (sampleRate / 100) * m_settings.m_squelchGate; // gate is given in 10s of ms at 48000 Hz audio sample rate
m_squelchCount = 0; // reset squelch open counter
m_ctcssDetector.setCoefficients(sampleRate/16, sampleRate/8.0f); // 0.5s / 2 Hz resolution
if (sampleRate < 16000) {
m_afSquelch.setCoefficients(sampleRate/2000, 600, sampleRate, 200, 0, afSqTones_lowrate); // 0.5ms test period, 300ms average span, audio SR, 100ms attack, no decay
} else {
m_afSquelch.setCoefficients(sampleRate/2000, 600, sampleRate, 200, 0, afSqTones); // 0.5ms test period, 300ms average span, audio SR, 100ms attack, no decay
}
m_discriCompensation = (sampleRate/48000.0f);
m_discriCompensation *= sqrt(m_discriCompensation);
m_phaseDiscri.setFMScaling(sampleRate / static_cast<float>(m_settings.m_fmDeviation));
m_audioFifo.setSize(sampleRate);
m_squelchDelayLine.resize(sampleRate/2);
m_audioSampleRate = sampleRate;
}

View File

@ -0,0 +1,180 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_NFMDEMODSINK_H
#define INCLUDE_NFMDEMODSINK_H
#include <vector>
#include "dsp/channelsamplesink.h"
#include "dsp/phasediscri.h"
#include "dsp/nco.h"
#include "dsp/interpolator.h"
#include "dsp/lowpass.h"
#include "dsp/bandpass.h"
#include "dsp/afsquelch.h"
#include "dsp/agc.h"
#include "dsp/ctcssdetector.h"
#include "util/movingaverage.h"
#include "util/doublebufferfifo.h"
#include "audio/audiofifo.h"
#include "nfmtestdemodsettings.h"
class NFMDemodSink : public ChannelSampleSink {
public:
NFMDemodSink();
~NFMDemodSink();
virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end);
const Real *getCtcssToneSet(int& nbTones) const {
nbTones = m_ctcssDetector.getNTones();
return m_ctcssDetector.getToneSet();
}
void setSelectedCtcssIndex(int selectedCtcssIndex) {
m_ctcssIndexSelected = selectedCtcssIndex;
}
bool getSquelchOpen() const { return m_squelchOpen; }
void getMagSqLevels(double& avg, double& peak, int& nbSamples)
{
if (m_magsqCount > 0)
{
m_magsq = m_magsqSum / m_magsqCount;
m_magSqLevelStore.m_magsq = m_magsq;
m_magSqLevelStore.m_magsqPeak = m_magsqPeak;
}
avg = m_magSqLevelStore.m_magsq;
peak = m_magSqLevelStore.m_magsqPeak;
nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount;
m_magsqSum = 0.0f;
m_magsqPeak = 0.0f;
m_magsqCount = 0;
}
void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false);
void applySettings(const NFMDemodTestSettings& settings, bool force = false);
void setMessageQueueToGUI(MessageQueue *messageQueue) { m_messageQueueToGUI = messageQueue; }
AudioFifo *getAudioFifo() { return &m_audioFifo; }
void applyAudioSampleRate(unsigned int sampleRate);
unsigned int getAudioSampleRate() const { return m_audioSampleRate; }
private:
struct MagSqLevelsStore
{
MagSqLevelsStore() :
m_magsq(1e-12),
m_magsqPeak(1e-12)
{}
double m_magsq;
double m_magsqPeak;
};
enum RateState {
RSInitialFill,
RSRunning
};
int m_channelSampleRate;
int m_channelFrequencyOffset;
NFMDemodTestSettings m_settings;
quint32 m_audioSampleRate;
AudioVector m_audioBuffer;
uint m_audioBufferFill;
AudioFifo m_audioFifo;
float m_discriCompensation; //!< compensation factor that depends on audio rate (1 for 48 kS/s)
NCO m_nco;
Interpolator m_interpolator;
Real m_interpolatorDistance;
Real m_interpolatorDistanceRemain;
Lowpass<Real> m_ctcssLowpass;
Bandpass<Real> m_bandpass;
Lowpass<Real> m_lowpass;
CTCSSDetector m_ctcssDetector;
int m_ctcssIndex; // 0 for nothing detected
int m_ctcssIndexSelected;
int m_sampleCount;
int m_squelchCount;
int m_squelchGate;
Real m_squelchLevel;
bool m_squelchOpen;
bool m_afSquelchOpen;
double m_magsq; //!< displayed averaged value
double m_magsqSum;
double m_magsqPeak;
int m_magsqCount;
MagSqLevelsStore m_magSqLevelStore;
MovingAverageUtil<Real, double, 32> m_movingAverage;
AFSquelch m_afSquelch;
Real m_agcLevel; // AGC will aim to this level
DoubleBufferFIFO<Real> m_squelchDelayLine;
PhaseDiscriminators m_phaseDiscri;
MessageQueue *m_messageQueueToGUI;
static const double afSqTones[];
static const double afSqTones_lowrate[];
void processOneSample(Complex &ci);
MessageQueue *getMessageQueueToGUI() { return m_messageQueueToGUI; }
inline float arctan2(Real y, Real x)
{
Real coeff_1 = M_PI / 4;
Real coeff_2 = 3 * coeff_1;
Real abs_y = fabs(y) + 1e-10; // kludge to prevent 0/0 condition
Real angle;
if( x>= 0) {
Real r = (x - abs_y) / (x + abs_y);
angle = coeff_1 - coeff_1 * r;
} else {
Real r = (x + abs_y) / (abs_y - x);
angle = coeff_2 - coeff_1 * r;
}
if(y < 0) {
return(-angle);
} else {
return(angle);
}
}
inline Real angleDist(Real a, Real b)
{
Real dist = b - a;
while(dist <= M_PI)
dist += 2 * M_PI;
while(dist >= M_PI)
dist -= 2 * M_PI;
return dist;
}
};
#endif // INCLUDE_NFMDEMODSINK_H

View File

@ -0,0 +1,542 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <complex.h>
#include <QTime>
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QBuffer>
#include <QThread>
#include "SWGChannelSettings.h"
#include "SWGNFMDemodSettings.h"
#include "SWGChannelReport.h"
#include "SWGNFMDemodReport.h"
#include "dsp/dspengine.h"
#include "dsp/dspcommands.h"
#include "dsp/devicesamplemimo.h"
#include "device/deviceapi.h"
#include "util/db.h"
#include "nfmtestdemod.h"
MESSAGE_CLASS_DEFINITION(NFMDemodTest::MsgConfigureNFMDemod, Message)
const QString NFMDemodTest::m_channelIdURI = "sdrangel.channel.nfmtestdemod";
const QString NFMDemodTest::m_channelId = "NFMTestDemod";
const int NFMDemodTest::m_udpBlockSize = 512;
NFMDemodTest::NFMDemodTest(DeviceAPI *devieAPI) :
ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink),
m_deviceAPI(devieAPI),
m_basebandSampleRate(0)
{
qDebug("NFMDemodTest::NFMDemodTest");
setObjectName(m_channelId);
m_thread = new QThread(this);
m_basebandSink = new NFMDemodBaseband();
m_basebandSink->moveToThread(m_thread);
applySettings(m_settings, true);
m_deviceAPI->addChannelSink(this);
m_deviceAPI->addChannelSinkAPI(this);
m_networkManager = new QNetworkAccessManager();
connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*)));
}
NFMDemodTest::~NFMDemodTest()
{
disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*)));
delete m_networkManager;
m_deviceAPI->removeChannelSinkAPI(this);
m_deviceAPI->removeChannelSink(this);
delete m_basebandSink;
delete m_thread;
}
uint32_t NFMDemodTest::getNumberOfDeviceStreams() const
{
return m_deviceAPI->getNbSourceStreams();
}
void NFMDemodTest::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst)
{
m_basebandSink->feed(begin, end);
}
void NFMDemodTest::start()
{
qDebug() << "NFMDemodTest::start";
if (m_basebandSampleRate != 0) {
m_basebandSink->setBasebandSampleRate(m_basebandSampleRate);
}
m_basebandSink->reset();
m_thread->start();
}
void NFMDemodTest::stop()
{
qDebug() << "NFMDemodTest::stop";
m_thread->exit();
m_thread->wait();
}
bool NFMDemodTest::handleMessage(const Message& cmd)
{
if (MsgConfigureNFMDemod::match(cmd))
{
MsgConfigureNFMDemod& cfg = (MsgConfigureNFMDemod&) cmd;
qDebug() << "NFMDemodTest::handleMessage: MsgConfigureNFMDemod";
applySettings(cfg.getSettings(), cfg.getForce());
return true;
}
else if (DSPSignalNotification::match(cmd))
{
DSPSignalNotification& notif = (DSPSignalNotification&) cmd;
m_basebandSampleRate = notif.getSampleRate();
// Forward to the sink
DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy
qDebug() << "NFMDemodTest::handleMessage: DSPSignalNotification";
m_basebandSink->getInputMessageQueue()->push(rep);
return true;
}
else
{
return false;
}
}
void NFMDemodTest::applySettings(const NFMDemodTestSettings& settings, bool force)
{
qDebug() << "NFMDemodTest::applySettings:"
<< " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset
<< " m_rfBandwidth: " << settings.m_rfBandwidth
<< " m_afBandwidth: " << settings.m_afBandwidth
<< " m_fmDeviation: " << settings.m_fmDeviation
<< " m_volume: " << settings.m_volume
<< " m_squelchGate: " << settings.m_squelchGate
<< " m_deltaSquelch: " << settings.m_deltaSquelch
<< " m_squelch: " << settings.m_squelch
<< " m_ctcssIndex: " << settings.m_ctcssIndex
<< " m_ctcssOn: " << settings.m_ctcssOn
<< " m_highPass: " << settings.m_highPass
<< " m_audioMute: " << settings.m_audioMute
<< " m_audioDeviceName: " << settings.m_audioDeviceName
<< " m_streamIndex: " << settings.m_streamIndex
<< " m_useReverseAPI: " << settings.m_useReverseAPI
<< " m_reverseAPIAddress: " << settings.m_reverseAPIAddress
<< " m_reverseAPIPort: " << settings.m_reverseAPIPort
<< " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex
<< " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex
<< " force: " << force;
QList<QString> reverseAPIKeys;
if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) {
reverseAPIKeys.append("inputFrequencyOffset");
}
if ((settings.m_volume != m_settings.m_volume) || force) {
reverseAPIKeys.append("volume");
}
if ((settings.m_ctcssOn != m_settings.m_ctcssOn) || force) {
reverseAPIKeys.append("ctcssOn");
}
if ((settings.m_audioMute != m_settings.m_audioMute) || force) {
reverseAPIKeys.append("audioMute");
}
if ((settings.m_rgbColor != m_settings.m_rgbColor) || force) {
reverseAPIKeys.append("rgbColor");
}
if ((settings.m_title != m_settings.m_title) || force) {
reverseAPIKeys.append("title");
}
if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) {
reverseAPIKeys.append("rfBandwidth");
}
if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) {
reverseAPIKeys.append("fmDeviation");
}
if ((settings.m_afBandwidth != m_settings.m_afBandwidth) || force) {
reverseAPIKeys.append("afBandwidth");
}
if ((settings.m_squelchGate != m_settings.m_squelchGate) || force) {
reverseAPIKeys.append("squelchGate");
}
if ((settings.m_squelch != m_settings.m_squelch) || force) {
reverseAPIKeys.append("squelch");
}
if ((settings.m_deltaSquelch != m_settings.m_deltaSquelch) || force) {
reverseAPIKeys.append("deltaSquelch");
}
if ((settings.m_ctcssIndex != m_settings.m_ctcssIndex) || force) {
reverseAPIKeys.append("ctcssIndex");
}
if ((settings.m_highPass != m_settings.m_highPass) || force) {
reverseAPIKeys.append("highPass");
}
if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) {
reverseAPIKeys.append("audioDeviceName");
}
if (m_settings.m_streamIndex != settings.m_streamIndex)
{
if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only
{
m_deviceAPI->removeChannelSinkAPI(this, m_settings.m_streamIndex);
m_deviceAPI->removeChannelSink(this, m_settings.m_streamIndex);
m_deviceAPI->addChannelSink(this, settings.m_streamIndex);
m_deviceAPI->addChannelSinkAPI(this, settings.m_streamIndex);
}
reverseAPIKeys.append("streamIndex");
}
NFMDemodBaseband::MsgConfigureNFMDemodBaseband *msg = NFMDemodBaseband::MsgConfigureNFMDemodBaseband::create(settings, force);
m_basebandSink->getInputMessageQueue()->push(msg);
if (settings.m_useReverseAPI)
{
bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) ||
(m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) ||
(m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) ||
(m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) ||
(m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex);
webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force);
}
m_settings = settings;
}
QByteArray NFMDemodTest::serialize() const
{
return m_settings.serialize();
}
bool NFMDemodTest::deserialize(const QByteArray& data)
{
bool success = true;
if (!m_settings.deserialize(data))
{
m_settings.resetToDefaults();
success = false;
}
MsgConfigureNFMDemod *msg = MsgConfigureNFMDemod::create(m_settings, true);
m_inputMessageQueue.push(msg);
return success;
}
int NFMDemodTest::webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
response.setNfmDemodSettings(new SWGSDRangel::SWGNFMDemodSettings());
response.getNfmDemodSettings()->init();
webapiFormatChannelSettings(response, m_settings);
return 200;
}
int NFMDemodTest::webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
NFMDemodTestSettings settings = m_settings;
webapiUpdateChannelSettings(settings, channelSettingsKeys, response);
MsgConfigureNFMDemod *msg = MsgConfigureNFMDemod::create(settings, force);
m_inputMessageQueue.push(msg);
if (m_guiMessageQueue) // forward to GUI if any
{
MsgConfigureNFMDemod *msgToGUI = MsgConfigureNFMDemod::create(settings, force);
m_guiMessageQueue->push(msgToGUI);
}
webapiFormatChannelSettings(response, settings);
return 200;
}
void NFMDemodTest::webapiUpdateChannelSettings(
NFMDemodTestSettings& settings,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response)
{
if (channelSettingsKeys.contains("afBandwidth")) {
settings.m_afBandwidth = response.getNfmDemodSettings()->getAfBandwidth();
}
if (channelSettingsKeys.contains("audioMute")) {
settings.m_audioMute = response.getNfmDemodSettings()->getAudioMute() != 0;
}
if (channelSettingsKeys.contains("highPass")) {
settings.m_highPass = response.getNfmDemodSettings()->getHighPass() != 0;
}
if (channelSettingsKeys.contains("ctcssIndex")) {
settings.m_ctcssIndex = response.getNfmDemodSettings()->getCtcssIndex();
}
if (channelSettingsKeys.contains("ctcssOn")) {
settings.m_ctcssOn = response.getNfmDemodSettings()->getCtcssOn() != 0;
}
if (channelSettingsKeys.contains("deltaSquelch")) {
settings.m_deltaSquelch = response.getNfmDemodSettings()->getDeltaSquelch() != 0;
}
if (channelSettingsKeys.contains("fmDeviation")) {
settings.m_fmDeviation = response.getNfmDemodSettings()->getFmDeviation();
}
if (channelSettingsKeys.contains("inputFrequencyOffset")) {
settings.m_inputFrequencyOffset = response.getNfmDemodSettings()->getInputFrequencyOffset();
}
if (channelSettingsKeys.contains("rfBandwidth")) {
settings.m_rfBandwidth = response.getNfmDemodSettings()->getRfBandwidth();
}
if (channelSettingsKeys.contains("rgbColor")) {
settings.m_rgbColor = response.getNfmDemodSettings()->getRgbColor();
}
if (channelSettingsKeys.contains("squelch")) {
settings.m_squelch = response.getNfmDemodSettings()->getSquelch();
}
if (channelSettingsKeys.contains("squelchGate")) {
settings.m_squelchGate = response.getNfmDemodSettings()->getSquelchGate();
}
if (channelSettingsKeys.contains("title")) {
settings.m_title = *response.getNfmDemodSettings()->getTitle();
}
if (channelSettingsKeys.contains("volume")) {
settings.m_volume = response.getNfmDemodSettings()->getVolume();
}
if (channelSettingsKeys.contains("audioDeviceName")) {
settings.m_audioDeviceName = *response.getNfmDemodSettings()->getAudioDeviceName();
}
if (channelSettingsKeys.contains("streamIndex")) {
settings.m_streamIndex = response.getNfmDemodSettings()->getStreamIndex();
}
if (channelSettingsKeys.contains("useReverseAPI")) {
settings.m_useReverseAPI = response.getNfmDemodSettings()->getUseReverseApi() != 0;
}
if (channelSettingsKeys.contains("reverseAPIAddress")) {
settings.m_reverseAPIAddress = *response.getNfmDemodSettings()->getReverseApiAddress();
}
if (channelSettingsKeys.contains("reverseAPIPort")) {
settings.m_reverseAPIPort = response.getNfmDemodSettings()->getReverseApiPort();
}
if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) {
settings.m_reverseAPIDeviceIndex = response.getNfmDemodSettings()->getReverseApiDeviceIndex();
}
if (channelSettingsKeys.contains("reverseAPIChannelIndex")) {
settings.m_reverseAPIChannelIndex = response.getNfmDemodSettings()->getReverseApiChannelIndex();
}
}
int NFMDemodTest::webapiReportGet(
SWGSDRangel::SWGChannelReport& response,
QString& errorMessage)
{
(void) errorMessage;
response.setNfmDemodReport(new SWGSDRangel::SWGNFMDemodReport());
response.getNfmDemodReport()->init();
webapiFormatChannelReport(response);
return 200;
}
void NFMDemodTest::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const NFMDemodTestSettings& settings)
{
response.getNfmDemodSettings()->setAfBandwidth(settings.m_afBandwidth);
response.getNfmDemodSettings()->setAudioMute(settings.m_audioMute ? 1 : 0);
response.getNfmDemodSettings()->setHighPass(settings.m_highPass ? 1 : 0);
response.getNfmDemodSettings()->setCtcssIndex(settings.m_ctcssIndex);
response.getNfmDemodSettings()->setCtcssOn(settings.m_ctcssOn ? 1 : 0);
response.getNfmDemodSettings()->setDeltaSquelch(settings.m_deltaSquelch ? 1 : 0);
response.getNfmDemodSettings()->setFmDeviation(settings.m_fmDeviation);
response.getNfmDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset);
response.getNfmDemodSettings()->setRfBandwidth(settings.m_rfBandwidth);
response.getNfmDemodSettings()->setRgbColor(settings.m_rgbColor);
response.getNfmDemodSettings()->setSquelch(settings.m_squelch);
response.getNfmDemodSettings()->setSquelchGate(settings.m_squelchGate);
response.getNfmDemodSettings()->setVolume(settings.m_volume);
if (response.getNfmDemodSettings()->getTitle()) {
*response.getNfmDemodSettings()->getTitle() = settings.m_title;
} else {
response.getNfmDemodSettings()->setTitle(new QString(settings.m_title));
}
if (response.getNfmDemodSettings()->getAudioDeviceName()) {
*response.getNfmDemodSettings()->getAudioDeviceName() = settings.m_audioDeviceName;
} else {
response.getNfmDemodSettings()->setAudioDeviceName(new QString(settings.m_audioDeviceName));
}
response.getNfmDemodSettings()->setStreamIndex(settings.m_streamIndex);
response.getNfmDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0);
if (response.getNfmDemodSettings()->getReverseApiAddress()) {
*response.getNfmDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress;
} else {
response.getNfmDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress));
}
response.getNfmDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort);
response.getNfmDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex);
response.getNfmDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex);
}
void NFMDemodTest::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response)
{
double magsqAvg, magsqPeak;
int nbMagsqSamples;
getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples);
response.getNfmDemodReport()->setChannelPowerDb(CalcDb::dbPower(magsqAvg));
int nbCtcssToneFrequencies;
const Real *ctcssToneFrequencies = m_basebandSink->getCtcssToneSet(nbCtcssToneFrequencies);
response.getNfmDemodReport()->setCtcssTone(
m_settings.m_ctcssOn ?
m_settings.m_ctcssIndex < 0 ?
0
: m_settings.m_ctcssIndex < nbCtcssToneFrequencies ?
ctcssToneFrequencies[m_settings.m_ctcssIndex-1]
: 0
: 0
);
response.getNfmDemodReport()->setSquelch(m_basebandSink->getSquelchOpen() ? 1 : 0);
response.getNfmDemodReport()->setAudioSampleRate(m_basebandSink->getAudioSampleRate());
response.getNfmDemodReport()->setChannelSampleRate(m_basebandSink->getChannelSampleRate());
}
void NFMDemodTest::webapiReverseSendSettings(QList<QString>& channelSettingsKeys, const NFMDemodTestSettings& settings, bool force)
{
SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings();
swgChannelSettings->setDirection(0); // single sink (Rx)
swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet());
swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex());
swgChannelSettings->setChannelType(new QString("NFMDemodTest"));
swgChannelSettings->setNfmDemodSettings(new SWGSDRangel::SWGNFMDemodSettings());
SWGSDRangel::SWGNFMDemodSettings *swgNFMDemodSettings = swgChannelSettings->getNfmDemodSettings();
// transfer data that has been modified. When force is on transfer all data except reverse API data
if (channelSettingsKeys.contains("afBandwidth") || force) {
swgNFMDemodSettings->setAfBandwidth(settings.m_afBandwidth);
}
if (channelSettingsKeys.contains("audioMute") || force) {
swgNFMDemodSettings->setAudioMute(settings.m_audioMute ? 1 : 0);
}
if (channelSettingsKeys.contains("highPass") || force) {
swgNFMDemodSettings->setAudioMute(settings.m_highPass ? 1 : 0);
}
if (channelSettingsKeys.contains("ctcssIndex") || force) {
swgNFMDemodSettings->setCtcssIndex(settings.m_ctcssIndex);
}
if (channelSettingsKeys.contains("ctcssOn") || force) {
swgNFMDemodSettings->setCtcssOn(settings.m_ctcssOn ? 1 : 0);
}
if (channelSettingsKeys.contains("deltaSquelch") || force) {
swgNFMDemodSettings->setDeltaSquelch(settings.m_deltaSquelch ? 1 : 0);
}
if (channelSettingsKeys.contains("fmDeviation") || force) {
swgNFMDemodSettings->setFmDeviation(settings.m_fmDeviation);
}
if (channelSettingsKeys.contains("inputFrequencyOffset") || force) {
swgNFMDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset);
}
if (channelSettingsKeys.contains("rfBandwidth") || force) {
swgNFMDemodSettings->setRfBandwidth(settings.m_rfBandwidth);
}
if (channelSettingsKeys.contains("rgbColor") || force) {
swgNFMDemodSettings->setRgbColor(settings.m_rgbColor);
}
if (channelSettingsKeys.contains("squelch") || force) {
swgNFMDemodSettings->setSquelch(settings.m_squelch);
}
if (channelSettingsKeys.contains("squelchGate") || force) {
swgNFMDemodSettings->setSquelchGate(settings.m_squelchGate);
}
if (channelSettingsKeys.contains("title") || force) {
swgNFMDemodSettings->setTitle(new QString(settings.m_title));
}
if (channelSettingsKeys.contains("volume") || force) {
swgNFMDemodSettings->setVolume(settings.m_volume);
}
if (channelSettingsKeys.contains("audioDeviceName") || force) {
swgNFMDemodSettings->setAudioDeviceName(new QString(settings.m_audioDeviceName));
}
if (channelSettingsKeys.contains("streamIndex") || force) {
swgNFMDemodSettings->setStreamIndex(settings.m_streamIndex);
}
QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings")
.arg(settings.m_reverseAPIAddress)
.arg(settings.m_reverseAPIPort)
.arg(settings.m_reverseAPIDeviceIndex)
.arg(settings.m_reverseAPIChannelIndex);
m_networkRequest.setUrl(QUrl(channelSettingsURL));
m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QBuffer *buffer = new QBuffer();
buffer->open((QBuffer::ReadWrite));
buffer->write(swgChannelSettings->asJson().toUtf8());
buffer->seek(0);
// Always use PATCH to avoid passing reverse API settings
QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer);
buffer->setParent(reply);
delete swgChannelSettings;
}
void NFMDemodTest::networkManagerFinished(QNetworkReply *reply)
{
QNetworkReply::NetworkError replyError = reply->error();
if (replyError)
{
qWarning() << "NFMDemodTest::networkManagerFinished:"
<< " error(" << (int) replyError
<< "): " << replyError
<< ": " << reply->errorString();
}
else
{
QString answer = reply->readAll();
answer.chop(1); // remove last \n
qDebug("NFMDemodTest::networkManagerFinished: reply:\n%s", answer.toStdString().c_str());
}
reply->deleteLater();
}

View File

@ -0,0 +1,150 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany //
// written by Christian Daniel //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_NFMTESTDEMOD_H
#define INCLUDE_NFMTESTDEMOD_H
#include <vector>
#include <QNetworkRequest>
#include "dsp/basebandsamplesink.h"
#include "channel/channelapi.h"
#include "util/message.h"
#include "nfmdemodbaseband.h"
#include "nfmtestdemodsettings.h"
class QNetworkAccessManager;
class QNetworkReply;
class QThread;
class DeviceAPI;
class NFMDemodBaseband;
class NFMDemodTest : public BasebandSampleSink, public ChannelAPI {
Q_OBJECT
public:
class MsgConfigureNFMDemod : public Message {
MESSAGE_CLASS_DECLARATION
public:
const NFMDemodTestSettings& getSettings() const { return m_settings; }
bool getForce() const { return m_force; }
static MsgConfigureNFMDemod* create(const NFMDemodTestSettings& settings, bool force)
{
return new MsgConfigureNFMDemod(settings, force);
}
private:
NFMDemodTestSettings m_settings;
bool m_force;
MsgConfigureNFMDemod(const NFMDemodTestSettings& settings, bool force) :
Message(),
m_settings(settings),
m_force(force)
{ }
};
NFMDemodTest(DeviceAPI *deviceAPI);
~NFMDemodTest();
virtual void destroy() { delete this; }
virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool positive);
virtual void start();
virtual void stop();
virtual bool handleMessage(const Message& cmd);
virtual void getIdentifier(QString& id) { id = objectName(); }
virtual void getTitle(QString& title) { title = m_settings.m_title; }
virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; }
virtual QByteArray serialize() const;
virtual bool deserialize(const QByteArray& data);
virtual int getNbSinkStreams() const { return 1; }
virtual int getNbSourceStreams() const { return 0; }
virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const
{
(void) streamIndex;
(void) sinkElseSource;
return m_settings.m_inputFrequencyOffset;
}
virtual int webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage);
virtual int webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage);
virtual int webapiReportGet(
SWGSDRangel::SWGChannelReport& response,
QString& errorMessage);
static void webapiFormatChannelSettings(
SWGSDRangel::SWGChannelSettings& response,
const NFMDemodTestSettings& settings);
static void webapiUpdateChannelSettings(
NFMDemodTestSettings& settings,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response);
const Real *getCtcssToneSet(int& nbTones) const { m_basebandSink->getCtcssToneSet(nbTones); }
void setSelectedCtcssIndex(int selectedCtcssIndex) { m_basebandSink->setSelectedCtcssIndex(selectedCtcssIndex); }
bool getSquelchOpen() const { return m_basebandSink->getSquelchOpen(); }
void getMagSqLevels(double& avg, double& peak, int& nbSamples) { m_basebandSink->getMagSqLevels(avg, peak, nbSamples); }
void propagateMessageQueueToGUI() { m_basebandSink->setMessageQueueToGUI(getMessageQueueToGUI()); }
uint32_t getNumberOfDeviceStreams() const;
static const QString m_channelIdURI;
static const QString m_channelId;
private:
enum RateState {
RSInitialFill,
RSRunning
};
DeviceAPI* m_deviceAPI;
QThread *m_thread;
NFMDemodBaseband* m_basebandSink;
NFMDemodTestSettings m_settings;
int m_basebandSampleRate; //!< stored from device message used when starting baseband sink
QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest;
static const int m_udpBlockSize;
void applySettings(const NFMDemodTestSettings& settings, bool force = false);
void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response);
void webapiReverseSendSettings(QList<QString>& channelSettingsKeys, const NFMDemodTestSettings& settings, bool force);
private slots:
void networkManagerFinished(QNetworkReply *reply);
};
#endif // INCLUDE_NFMDEMOD_H

View File

@ -0,0 +1,509 @@
#include "nfmtestdemodgui.h"
#include "device/deviceuiset.h"
#include <QDockWidget>
#include <QMainWindow>
#include <QDebug>
#include "ui_nfmtestdemodgui.h"
#include "plugin/pluginapi.h"
#include "util/simpleserializer.h"
#include "util/db.h"
#include "gui/basicchannelsettingsdialog.h"
#include "gui/devicestreamselectiondialog.h"
#include "gui/crightclickenabler.h"
#include "gui/audioselectdialog.h"
#include "dsp/dspengine.h"
#include "mainwindow.h"
#include "nfmdemodreport.h"
#include "nfmtestdemod.h"
NFMDemodTestGUI* NFMDemodTestGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel)
{
NFMDemodTestGUI* gui = new NFMDemodTestGUI(pluginAPI, deviceUISet, rxChannel);
return gui;
}
void NFMDemodTestGUI::destroy()
{
delete this;
}
void NFMDemodTestGUI::setName(const QString& name)
{
setObjectName(name);
}
QString NFMDemodTestGUI::getName() const
{
return objectName();
}
qint64 NFMDemodTestGUI::getCenterFrequency() const
{
return m_channelMarker.getCenterFrequency();
}
void NFMDemodTestGUI::setCenterFrequency(qint64 centerFrequency)
{
m_channelMarker.setCenterFrequency(centerFrequency);
applySettings();
}
void NFMDemodTestGUI::resetToDefaults()
{
m_settings.resetToDefaults();
displaySettings();
applySettings();
}
QByteArray NFMDemodTestGUI::serialize() const
{
return m_settings.serialize();
}
bool NFMDemodTestGUI::deserialize(const QByteArray& data)
{
if(m_settings.deserialize(data)) {
displaySettings();
applySettings(true);
return true;
} else {
resetToDefaults();
return false;
}
}
bool NFMDemodTestGUI::handleMessage(const Message& message)
{
if (NFMDemodReport::MsgReportCTCSSFreq::match(message))
{
NFMDemodReport::MsgReportCTCSSFreq& report = (NFMDemodReport::MsgReportCTCSSFreq&) message;
setCtcssFreq(report.getFrequency());
return true;
}
else if (NFMDemodTest::MsgConfigureNFMDemod::match(message))
{
qDebug("NFMDemodTestGUI::handleMessage: NFMDemodTest::MsgConfigureNFMDemod");
const NFMDemodTest::MsgConfigureNFMDemod& cfg = (NFMDemodTest::MsgConfigureNFMDemod&) message;
m_settings = cfg.getSettings();
blockApplySettings(true);
displaySettings();
blockApplySettings(false);
return true;
}
return false;
}
void NFMDemodTestGUI::handleInputMessages()
{
Message* message;
while ((message = getInputMessageQueue()->pop()) != 0)
{
if (handleMessage(*message))
{
delete message;
}
}
}
void NFMDemodTestGUI::channelMarkerChangedByCursor()
{
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
applySettings();
}
void NFMDemodTestGUI::channelMarkerHighlightedByCursor()
{
setHighlighted(m_channelMarker.getHighlighted());
}
void NFMDemodTestGUI::on_deltaFrequency_changed(qint64 value)
{
m_channelMarker.setCenterFrequency(value);
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
applySettings();
}
void NFMDemodTestGUI::on_rfBW_currentIndexChanged(int index)
{
qDebug() << "NFMDemodTestGUI::on_rfBW_currentIndexChanged" << index;
//ui->rfBWText->setText(QString("%1 k").arg(m_rfBW[value] / 1000.0));
m_channelMarker.setBandwidth(NFMDemodTestSettings::getRFBW(index));
m_settings.m_rfBandwidth = NFMDemodTestSettings::getRFBW(index);
m_settings.m_fmDeviation = NFMDemodTestSettings::getFMDev(index);
applySettings();
}
void NFMDemodTestGUI::on_afBW_valueChanged(int value)
{
ui->afBWText->setText(QString("%1 k").arg(value));
m_settings.m_afBandwidth = value * 1000.0;
applySettings();
}
void NFMDemodTestGUI::on_volume_valueChanged(int value)
{
ui->volumeText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1));
m_settings.m_volume = value / 10.0;
applySettings();
}
void NFMDemodTestGUI::on_squelchGate_valueChanged(int value)
{
ui->squelchGateText->setText(QString("%1").arg(value * 10.0f, 0, 'f', 0));
m_settings.m_squelchGate = value;
applySettings();
}
void NFMDemodTestGUI::on_deltaSquelch_toggled(bool checked)
{
if (checked)
{
ui->squelchText->setText(QString("%1").arg((-ui->squelch->value()) / 1.0, 0, 'f', 0));
ui->squelchText->setToolTip(tr("Squelch AF balance threshold (%)"));
ui->squelch->setToolTip(tr("Squelch AF balance threshold (%)"));
}
else
{
ui->squelchText->setText(QString("%1").arg(ui->squelch->value() / 1.0, 0, 'f', 0));
ui->squelchText->setToolTip(tr("Squelch power threshold (dB)"));
ui->squelch->setToolTip(tr("Squelch power threshold (dB)"));
}
m_settings.m_deltaSquelch = checked;
applySettings();
}
void NFMDemodTestGUI::on_squelch_valueChanged(int value)
{
if (ui->deltaSquelch->isChecked())
{
ui->squelchText->setText(QString("%1").arg(-value / 1.0, 0, 'f', 0));
ui->squelchText->setToolTip(tr("Squelch AF balance threshold (%)"));
ui->squelch->setToolTip(tr("Squelch AF balance threshold (%)"));
}
else
{
ui->squelchText->setText(QString("%1").arg(value / 1.0, 0, 'f', 0));
ui->squelchText->setToolTip(tr("Squelch power threshold (dB)"));
ui->squelch->setToolTip(tr("Squelch power threshold (dB)"));
}
m_settings.m_squelch = value * 1.0;
applySettings();
}
void NFMDemodTestGUI::on_ctcssOn_toggled(bool checked)
{
m_settings.m_ctcssOn = checked;
applySettings();
}
void NFMDemodTestGUI::on_highPassFilter_toggled(bool checked)
{
m_settings.m_highPass = checked;
applySettings();
}
void NFMDemodTestGUI::on_audioMute_toggled(bool checked)
{
m_settings.m_audioMute = checked;
applySettings();
}
void NFMDemodTestGUI::on_ctcss_currentIndexChanged(int index)
{
m_settings.m_ctcssIndex = index;
applySettings();
}
void NFMDemodTestGUI::onWidgetRolled(QWidget* widget, bool rollDown)
{
(void) widget;
(void) rollDown;
/*
if((widget == ui->spectrumContainer) && (m_nfmDemod != NULL))
m_nfmDemod->setSpectrum(m_threadedSampleSink->getMessageQueue(), rollDown);
*/
}
void NFMDemodTestGUI::onMenuDialogCalled(const QPoint &p)
{
if (m_contextMenuType == ContextMenuChannelSettings)
{
BasicChannelSettingsDialog dialog(&m_channelMarker, this);
dialog.setUseReverseAPI(m_settings.m_useReverseAPI);
dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress);
dialog.setReverseAPIPort(m_settings.m_reverseAPIPort);
dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex);
dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex);
dialog.move(p);
dialog.exec();
m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency();
m_settings.m_rgbColor = m_channelMarker.getColor().rgb();
m_settings.m_title = m_channelMarker.getTitle();
m_settings.m_useReverseAPI = dialog.useReverseAPI();
m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress();
m_settings.m_reverseAPIPort = dialog.getReverseAPIPort();
m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex();
m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex();
setWindowTitle(m_settings.m_title);
setTitleColor(m_settings.m_rgbColor);
applySettings();
}
else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine))
{
DeviceStreamSelectionDialog dialog(this);
dialog.setNumberOfStreams(m_nfmDemod->getNumberOfDeviceStreams());
dialog.setStreamIndex(m_settings.m_streamIndex);
dialog.move(p);
dialog.exec();
m_settings.m_streamIndex = dialog.getSelectedStreamIndex();
m_channelMarker.clearStreamIndexes();
m_channelMarker.addStreamIndex(m_settings.m_streamIndex);
displayStreamIndex();
applySettings();
}
resetContextMenuType();
}
NFMDemodTestGUI::NFMDemodTestGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) :
RollupWidget(parent),
ui(new Ui::NFMDemodTestGUI),
m_pluginAPI(pluginAPI),
m_deviceUISet(deviceUISet),
m_channelMarker(this),
m_basicSettingsShown(false),
m_doApplySettings(true),
m_squelchOpen(false),
m_tickCount(0)
{
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose, true);
connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
m_nfmDemod = reinterpret_cast<NFMDemodTest*>(rxChannel); //new NFMDemodTest(m_deviceUISet->m_deviceSourceAPI);
m_nfmDemod->setMessageQueueToGUI(getInputMessageQueue());
m_nfmDemod->propagateMessageQueueToGUI();
connect(&MainWindow::getInstance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick()));
CRightClickEnabler *audioMuteRightClickEnabler = new CRightClickEnabler(ui->audioMute);
connect(audioMuteRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(audioSelect()));
blockApplySettings(true);
ui->rfBW->clear();
for (int i = 0; i < NFMDemodTestSettings::m_nbRfBW; i++) {
ui->rfBW->addItem(QString("%1").arg(NFMDemodTestSettings::getRFBW(i) / 1000.0, 0, 'f', 2));
}
int ctcss_nbTones;
const Real *ctcss_tones = m_nfmDemod->getCtcssToneSet(ctcss_nbTones);
ui->ctcss->addItem("--");
for (int i=0; i<ctcss_nbTones; i++)
{
ui->ctcss->addItem(QString("%1").arg(ctcss_tones[i]));
}
blockApplySettings(false);
ui->audioMute->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); // squelch closed
ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03)));
ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold));
ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999);
ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue);
m_channelMarker.blockSignals(true);
m_channelMarker.setColor(Qt::red);
m_channelMarker.setBandwidth(5000);
m_channelMarker.setCenterFrequency(0);
m_channelMarker.setTitle("NFM Test Demodulator");
m_channelMarker.blockSignals(false);
m_channelMarker.setVisible(true); // activate signal on the last setting only
m_settings.setChannelMarker(&m_channelMarker);
m_deviceUISet->registerRxChannelInstance(NFMDemodTest::m_channelIdURI, this);
m_deviceUISet->addChannelMarker(&m_channelMarker);
m_deviceUISet->addRollupWidget(this);
connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor()));
connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor()));
QChar delta = QChar(0x94, 0x03);
ui->deltaSquelch->setText(delta);
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
displaySettings();
applySettings(true);
}
NFMDemodTestGUI::~NFMDemodTestGUI()
{
m_deviceUISet->removeRxChannelInstance(this);
delete m_nfmDemod; // TODO: check this: when the GUI closes it has to delete the demodulator
delete ui;
}
void NFMDemodTestGUI::applySettings(bool force)
{
if (m_doApplySettings)
{
qDebug() << "NFMDemodTestGUI::applySettings";
NFMDemodTest::MsgConfigureNFMDemod* message = NFMDemodTest::MsgConfigureNFMDemod::create( m_settings, force);
m_nfmDemod->getInputMessageQueue()->push(message);
}
}
void NFMDemodTestGUI::displaySettings()
{
m_channelMarker.blockSignals(true);
m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset);
m_channelMarker.setBandwidth(m_settings.m_rfBandwidth);
m_channelMarker.setTitle(m_settings.m_title);
m_channelMarker.blockSignals(false);
m_channelMarker.setColor(m_settings.m_rgbColor);
setTitleColor(m_settings.m_rgbColor);
setWindowTitle(m_channelMarker.getTitle());
blockApplySettings(true);
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
ui->rfBW->setCurrentIndex(NFMDemodTestSettings::getRFBWIndex(m_settings.m_rfBandwidth));
ui->afBWText->setText(QString("%1 k").arg(m_settings.m_afBandwidth / 1000.0));
ui->afBW->setValue(m_settings.m_afBandwidth / 1000.0);
ui->volumeText->setText(QString("%1").arg(m_settings.m_volume, 0, 'f', 1));
ui->volume->setValue(m_settings.m_volume * 10.0);
ui->squelchGateText->setText(QString("%1").arg(m_settings.m_squelchGate * 10.0f, 0, 'f', 0));
ui->squelchGate->setValue(m_settings.m_squelchGate);
ui->deltaSquelch->setChecked(m_settings.m_deltaSquelch);
ui->squelch->setValue(m_settings.m_squelch * 1.0);
if (m_settings.m_deltaSquelch)
{
ui->squelchText->setText(QString("%1").arg((-m_settings.m_squelch) / 1.0, 0, 'f', 0));
ui->squelchText->setToolTip(tr("Squelch AF balance threshold (%)"));
ui->squelch->setToolTip(tr("Squelch AF balance threshold (%)"));
}
else
{
ui->squelchText->setText(QString("%1").arg(m_settings.m_squelch / 1.0, 0, 'f', 0));
ui->squelchText->setToolTip(tr("Squelch power threshold (dB)"));
ui->squelch->setToolTip(tr("Squelch power threshold (dB)"));
}
ui->ctcssOn->setChecked(m_settings.m_ctcssOn);
ui->highPassFilter->setChecked(m_settings.m_highPass);
ui->audioMute->setChecked(m_settings.m_audioMute);
ui->ctcss->setCurrentIndex(m_settings.m_ctcssIndex);
displayStreamIndex();
blockApplySettings(false);
}
void NFMDemodTestGUI::displayStreamIndex()
{
if (m_deviceUISet->m_deviceMIMOEngine) {
setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex));
} else {
setStreamIndicator("S"); // single channel indicator
}
}
void NFMDemodTestGUI::leaveEvent(QEvent*)
{
m_channelMarker.setHighlighted(false);
}
void NFMDemodTestGUI::enterEvent(QEvent*)
{
m_channelMarker.setHighlighted(true);
}
void NFMDemodTestGUI::setCtcssFreq(Real ctcssFreq)
{
if (ctcssFreq == 0)
{
ui->ctcssText->setText("--");
}
else
{
ui->ctcssText->setText(QString("%1").arg(ctcssFreq));
}
}
void NFMDemodTestGUI::blockApplySettings(bool block)
{
m_doApplySettings = !block;
}
void NFMDemodTestGUI::audioSelect()
{
qDebug("NFMDemodTestGUI::audioSelect");
AudioSelectDialog audioSelect(DSPEngine::instance()->getAudioDeviceManager(), m_settings.m_audioDeviceName);
audioSelect.exec();
if (audioSelect.m_selected)
{
m_settings.m_audioDeviceName = audioSelect.m_audioDeviceName;
applySettings();
}
}
void NFMDemodTestGUI::tick()
{
double magsqAvg, magsqPeak;
int nbMagsqSamples;
m_nfmDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples);
double powDbAvg = CalcDb::dbPower(magsqAvg);
double powDbPeak = CalcDb::dbPower(magsqPeak);
ui->channelPowerMeter->levelChanged(
(100.0f + powDbAvg) / 100.0f,
(100.0f + powDbPeak) / 100.0f,
nbMagsqSamples);
if (m_tickCount % 4 == 0) {
ui->channelPower->setText(tr("%1 dB").arg(powDbAvg, 0, 'f', 1));
}
bool squelchOpen = m_nfmDemod->getSquelchOpen();
if (squelchOpen != m_squelchOpen)
{
if (squelchOpen) {
ui->audioMute->setStyleSheet("QToolButton { background-color : green; }");
} else {
ui->audioMute->setStyleSheet("QToolButton { background:rgb(79,79,79); }");
}
m_squelchOpen = squelchOpen;
}
m_tickCount++;
}

View File

@ -0,0 +1,89 @@
#ifndef INCLUDE_NFMTESTDEMODGUI_H
#define INCLUDE_NFMTESTDEMODGUI_H
#include <plugin/plugininstancegui.h>
#include "gui/rollupwidget.h"
#include "dsp/dsptypes.h"
#include "dsp/channelmarker.h"
#include "dsp/movingaverage.h"
#include "util/messagequeue.h"
#include "nfmtestdemodsettings.h"
class PluginAPI;
class DeviceUISet;
class BasebandSampleSink;
class NFMDemodTest;
namespace Ui {
class NFMDemodTestGUI;
}
class NFMDemodTestGUI : public RollupWidget, public PluginInstanceGUI {
Q_OBJECT
public:
static NFMDemodTestGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel);
virtual void destroy();
void setName(const QString& name);
QString getName() const;
virtual qint64 getCenterFrequency() const;
virtual void setCenterFrequency(qint64 centerFrequency);
void resetToDefaults();
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; }
virtual bool handleMessage(const Message& message);
void setCtcssFreq(Real ctcssFreq);
public slots:
void channelMarkerChangedByCursor();
void channelMarkerHighlightedByCursor();
private:
Ui::NFMDemodTestGUI* ui;
PluginAPI* m_pluginAPI;
DeviceUISet* m_deviceUISet;
ChannelMarker m_channelMarker;
NFMDemodTestSettings m_settings;
bool m_basicSettingsShown;
bool m_doApplySettings;
NFMDemodTest* m_nfmDemod;
bool m_squelchOpen;
uint32_t m_tickCount;
MessageQueue m_inputMessageQueue;
explicit NFMDemodTestGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0);
virtual ~NFMDemodTestGUI();
void blockApplySettings(bool block);
void applySettings(bool force = false);
void displaySettings();
void displayStreamIndex();
void leaveEvent(QEvent*);
void enterEvent(QEvent*);
private slots:
void on_deltaFrequency_changed(qint64 value);
void on_rfBW_currentIndexChanged(int index);
void on_afBW_valueChanged(int value);
void on_volume_valueChanged(int value);
void on_squelchGate_valueChanged(int value);
void on_deltaSquelch_toggled(bool checked);
void on_squelch_valueChanged(int value);
void on_ctcss_currentIndexChanged(int index);
void on_ctcssOn_toggled(bool checked);
void on_highPassFilter_toggled(bool checked);
void on_audioMute_toggled(bool checked);
void onWidgetRolled(QWidget* widget, bool rollDown);
void onMenuDialogCalled(const QPoint& p);
void handleInputMessages();
void audioSelect();
void tick();
};
#endif // INCLUDE_NFMTESTDEMODGUI_H

View File

@ -0,0 +1,643 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NFMDemodTestGUI</class>
<widget class="RollupWidget" name="NFMDemodTestGUI">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>302</width>
<height>178</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>302</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="windowTitle">
<string>NFM Test Demodulator</string>
</property>
<widget class="QWidget" name="settingsContainer" native="true">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>141</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<layout class="QHBoxLayout" name="deltaFreqPowerLayout">
<property name="topMargin">
<number>2</number>
</property>
<item>
<layout class="QHBoxLayout" name="DeltaFrequencyLayout">
<item>
<widget class="QLabel" name="deltaFrequencyLabel">
<property name="minimumSize">
<size>
<width>16</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Df</string>
</property>
</widget>
</item>
<item>
<widget class="ValueDialZ" name="deltaFrequency" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>32</width>
<height>16</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Mono</family>
<pointsize>12</pointsize>
</font>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="toolTip">
<string>Demod shift frequency from center in Hz</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="deltaUnits">
<property name="text">
<string>Hz </string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="channelPower">
<property name="minimumSize">
<size>
<width>60</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Channel power</string>
</property>
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>-100.0 dB</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="levelMeterLayout">
<item>
<widget class="QLabel" name="channelPowerMeterUnits">
<property name="text">
<string>dB</string>
</property>
</widget>
</item>
<item>
<widget class="LevelMeterSignalDB" name="channelPowerMeter" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>24</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Mono</family>
<pointsize>8</pointsize>
</font>
</property>
<property name="toolTip">
<string>Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="rfBWLayout">
<item>
<widget class="QLabel" name="rfBWLabel">
<property name="text">
<string>RFBW</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="rfBW">
<property name="maximumSize">
<size>
<width>60</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>RF demodulator bandwidth (kHz)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="rfBWUnitText">
<property name="minimumSize">
<size>
<width>10</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>k</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="bwSpacer">
<property name="minimumSize">
<size>
<width>10</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="afLabel">
<property name="text">
<string>AFBW</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="afBW">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Audio bandwidth (kHz)</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>20</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>3</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="afBWText">
<property name="minimumSize">
<size>
<width>25</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>3 k</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="squelchLayout">
<item>
<widget class="QLabel" name="volumeLabel">
<property name="text">
<string>Vol</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="volume">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Sound volume</string>
</property>
<property name="maximum">
<number>40</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="volumeText">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Sound volume</string>
</property>
<property name="text">
<string>1.0</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="squelchLabel">
<property name="text">
<string>Sq</string>
</property>
</widget>
</item>
<item>
<widget class="ButtonSwitch" name="deltaSquelch">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Toggle AF balance (on) or channel power (off) based squelch</string>
</property>
<property name="text">
<string>D</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="squelch">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Squelch threshold (dB)</string>
</property>
<property name="minimum">
<number>-100</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>-100</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="squelchText">
<property name="minimumSize">
<size>
<width>28</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Squelch threshold (dB)</string>
</property>
<property name="text">
<string>-100</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="squelchGate">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Squelch gate (ms)</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>50</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>5</number>
</property>
<property name="sliderPosition">
<number>5</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="squelchGateText">
<property name="minimumSize">
<size>
<width>25</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Squelch gate (ms)</string>
</property>
<property name="text">
<string>000</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="ctcssLayout">
<item>
<widget class="QLabel" name="ctcssLabel">
<property name="text">
<string>CTCSS</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="CTCSSblock">
<item>
<widget class="QCheckBox" name="ctcssOn">
<property name="toolTip">
<string>Activate CTCSS</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="ctcss">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string>Set CTCSS Frequency</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="ctcssText">
<property name="toolTip">
<string>CTCSS detected</string>
</property>
<property name="text">
<string>--</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="ctcssSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="ButtonSwitch" name="highPassFilter">
<property name="toolTip">
<string>High pass audio filter</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/filter_highpass.png</normaloff>:/filter_highpass.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="audioMute">
<property name="toolTip">
<string>Left: Mute/Unmute audio Right: Select audio output device</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/sound_on.png</normaloff>
<normalon>:/sound_off.png</normalon>:/sound_on.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>RollupWidget</class>
<extends>QWidget</extends>
<header>gui/rollupwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>LevelMeterSignalDB</class>
<extends>QWidget</extends>
<header>gui/levelmeter.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<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>
</customwidgets>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,197 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2017 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QColor>
#include "dsp/dspengine.h"
#include "util/simpleserializer.h"
#include "settings/serializable.h"
#include "nfmtestdemodsettings.h"
const int NFMDemodTestSettings::m_rfBW[] = {
5000, 6250, 8330, 10000, 12500, 15000, 20000, 25000, 40000
};
const int NFMDemodTestSettings::m_fmDev[] = { // corresponding single side FM deviations at 0.4 * BW
2000, 2500, 3330, 4000, 5000, 6000, 8000, 10000, 16000
};
const int NFMDemodTestSettings::m_nbRfBW = 9;
NFMDemodTestSettings::NFMDemodTestSettings() :
m_channelMarker(0)
{
resetToDefaults();
}
void NFMDemodTestSettings::resetToDefaults()
{
m_inputFrequencyOffset = 0;
m_rfBandwidth = 12500;
m_afBandwidth = 3000;
m_fmDeviation = 2000;
m_squelchGate = 5; // 10s of ms at 48000 Hz sample rate. Corresponds to 2400 for AGC attack
m_deltaSquelch = false;
m_squelch = -30.0;
m_volume = 1.0;
m_ctcssOn = false;
m_audioMute = false;
m_ctcssIndex = 0;
m_rgbColor = QColor(255, 0, 0).rgb();
m_title = "NFM Demodulator";
m_audioDeviceName = AudioDeviceManager::m_defaultDeviceName;
m_highPass = true;
m_streamIndex = 0;
m_useReverseAPI = false;
m_reverseAPIAddress = "127.0.0.1";
m_reverseAPIPort = 8888;
m_reverseAPIDeviceIndex = 0;
m_reverseAPIChannelIndex = 0;
}
QByteArray NFMDemodTestSettings::serialize() const
{
SimpleSerializer s(1);
s.writeS32(1, m_inputFrequencyOffset);
s.writeS32(2, getRFBWIndex(m_rfBandwidth));
s.writeS32(3, m_afBandwidth/1000.0);
s.writeS32(4, m_volume*10.0);
s.writeS32(5, static_cast<int>(m_squelch));
s.writeBool(6, m_highPass);
s.writeU32(7, m_rgbColor);
s.writeS32(8, m_ctcssIndex);
s.writeBool(9, m_ctcssOn);
s.writeBool(10, m_audioMute);
s.writeS32(11, m_squelchGate);
s.writeBool(12, m_deltaSquelch);
if (m_channelMarker) {
s.writeBlob(13, m_channelMarker->serialize());
}
s.writeString(14, m_title);
s.writeString(15, m_audioDeviceName);
s.writeBool(16, m_useReverseAPI);
s.writeString(17, m_reverseAPIAddress);
s.writeU32(18, m_reverseAPIPort);
s.writeU32(19, m_reverseAPIDeviceIndex);
s.writeU32(20, m_reverseAPIChannelIndex);
s.writeS32(21, m_streamIndex);
return s.final();
}
bool NFMDemodTestSettings::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid())
{
resetToDefaults();
return false;
}
if (d.getVersion() == 1)
{
QByteArray bytetmp;
qint32 tmp;
uint32_t utmp;
if (m_channelMarker)
{
d.readBlob(13, &bytetmp);
m_channelMarker->deserialize(bytetmp);
}
d.readS32(1, &tmp, 0);
m_inputFrequencyOffset = tmp;
d.readS32(2, &tmp, 4);
m_rfBandwidth = getRFBW(tmp);
m_fmDeviation = getFMDev(tmp);
d.readS32(3, &tmp, 3);
m_afBandwidth = tmp * 1000.0;
d.readS32(4, &tmp, 20);
m_volume = tmp / 10.0;
d.readS32(5, &tmp, -30);
m_squelch = (tmp < -100 ? tmp/10 : tmp) * 1.0;
d.readBool(6, &m_highPass, true);
d.readU32(7, &m_rgbColor, QColor(255, 0, 0).rgb());
d.readS32(8, &m_ctcssIndex, 0);
d.readBool(9, &m_ctcssOn, false);
d.readBool(10, &m_audioMute, false);
d.readS32(11, &m_squelchGate, 5);
d.readBool(12, &m_deltaSquelch, false);
d.readString(14, &m_title, "NFM Test Demodulator");
d.readString(15, &m_audioDeviceName, AudioDeviceManager::m_defaultDeviceName);
d.readBool(16, &m_useReverseAPI, false);
d.readString(17, &m_reverseAPIAddress, "127.0.0.1");
d.readU32(18, &utmp, 0);
if ((utmp > 1023) && (utmp < 65535)) {
m_reverseAPIPort = utmp;
} else {
m_reverseAPIPort = 8888;
}
d.readU32(19, &utmp, 0);
m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp;
d.readU32(20, &utmp, 0);
m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp;
d.readS32(21, &m_streamIndex, 0);
return true;
}
else
{
resetToDefaults();
return false;
}
}
int NFMDemodTestSettings::getRFBW(int index)
{
if (index < 0) {
return m_rfBW[0];
} else if (index < m_nbRfBW) {
return m_rfBW[index];
} else {
return m_rfBW[m_nbRfBW-1];
}
}
int NFMDemodTestSettings::getFMDev(int index)
{
if (index < 0) {
return m_fmDev[0];
} else if (index < m_nbRfBW) {
return m_fmDev[index];
} else {
return m_fmDev[m_nbRfBW-1];
}
}
int NFMDemodTestSettings::getRFBWIndex(int rfbw)
{
for (int i = 0; i < m_nbRfBW; i++)
{
if (rfbw <= m_rfBW[i])
{
return i;
}
}
return m_nbRfBW-1;
}

View File

@ -0,0 +1,68 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2017 Edouard Griffiths, F4EXB //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef PLUGINS_CHANNELRX_DEMODNFMTEST_NFMDEMODTESTSETTINGS_H_
#define PLUGINS_CHANNELRX_DEMODNFMTEST_NFMDEMODTESTSETTINGS_H_
#include <stdint.h>
class Serializable;
struct NFMDemodTestSettings
{
static const int m_nbRfBW;
static const int m_rfBW[];
static const int m_fmDev[];
int32_t m_inputFrequencyOffset;
Real m_rfBandwidth;
Real m_afBandwidth;
int m_fmDeviation;
int m_squelchGate;
bool m_deltaSquelch;
Real m_squelch; //!< deci-Bels
Real m_volume;
bool m_ctcssOn;
bool m_audioMute;
int m_ctcssIndex;
quint32 m_rgbColor;
QString m_title;
QString m_audioDeviceName;
bool m_highPass;
int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx).
bool m_useReverseAPI;
QString m_reverseAPIAddress;
uint16_t m_reverseAPIPort;
uint16_t m_reverseAPIDeviceIndex;
uint16_t m_reverseAPIChannelIndex;
Serializable *m_channelMarker;
NFMDemodTestSettings();
void resetToDefaults();
void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; }
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
static int getRFBW(int index);
static int getFMDev(int index);
static int getRFBWIndex(int rfbw);
};
#endif /* PLUGINS_CHANNELRX_DEMODNFMTEST_NFMDEMODTESTSETTINGS_H_ */

View File

@ -0,0 +1,50 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB. //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "SWGChannelSettings.h"
#include "nfmtestdemod.h"
#include "nfmtestdemodwebapiadapter.h"
NFMDemodTestWebAPIAdapter::NFMDemodTestWebAPIAdapter()
{}
NFMDemodTestWebAPIAdapter::~NFMDemodTestWebAPIAdapter()
{}
int NFMDemodTestWebAPIAdapter::webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
response.setNfmDemodSettings(new SWGSDRangel::SWGNFMDemodSettings());
response.getNfmDemodSettings()->init();
NFMDemodTest::webapiFormatChannelSettings(response, m_settings);
return 200;
}
int NFMDemodTestWebAPIAdapter::webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage)
{
(void) errorMessage;
NFMDemodTest::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response);
return 200;
}

View File

@ -0,0 +1,49 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2019 Edouard Griffiths, F4EXB. //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_NFMTESTDEMOD_WEBAPIADAPTER_H
#define INCLUDE_NFMTESTDEMOD_WEBAPIADAPTER_H
#include "channel/channelwebapiadapter.h"
#include "nfmtestdemodsettings.h"
/**
* Standalone API adapter only for the settings
*/
class NFMDemodTestWebAPIAdapter : public ChannelWebAPIAdapter {
public:
NFMDemodTestWebAPIAdapter();
virtual ~NFMDemodTestWebAPIAdapter();
virtual QByteArray serialize() const { return m_settings.serialize(); }
virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); }
virtual int webapiSettingsGet(
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage);
virtual int webapiSettingsPutPatch(
bool force,
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response,
QString& errorMessage);
private:
NFMDemodTestSettings m_settings;
};
#endif // INCLUDE_NFMTESTDEMOD_WEBAPIADAPTER_H

View File

@ -0,0 +1,66 @@
#include <QtPlugin>
#include "plugin/pluginapi.h"
#ifndef SERVER_MODE
#include "nfmtestdemodgui.h"
#endif
#include "nfmtestdemod.h"
#include "nfmtestdemodwebapiadapter.h"
#include "nfmtestplugin.h"
const PluginDescriptor NFMTestPlugin::m_pluginDescriptor = {
QString("NFM Test Demodulator"),
QString("4.12.1"),
QString("(c) Edouard Griffiths, F4EXB"),
QString("https://github.com/f4exb/sdrangel"),
true,
QString("https://github.com/f4exb/sdrangel")
};
NFMTestPlugin::NFMTestPlugin(QObject* parent) :
QObject(parent),
m_pluginAPI(0)
{
}
const PluginDescriptor& NFMTestPlugin::getPluginDescriptor() const
{
return m_pluginDescriptor;
}
void NFMTestPlugin::initPlugin(PluginAPI* pluginAPI)
{
m_pluginAPI = pluginAPI;
// register NFM demodulator
m_pluginAPI->registerRxChannel(NFMDemodTest::m_channelIdURI, NFMDemodTest::m_channelId, this);
}
#ifdef SERVER_MODE
PluginInstanceGUI* NFMTestPlugin::createRxChannelGUI(
DeviceUISet *deviceUISet,
BasebandSampleSink *rxChannel) const
{
return 0;
}
#else
PluginInstanceGUI* NFMTestPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const
{
return NFMDemodTestGUI::create(m_pluginAPI, deviceUISet, rxChannel);
}
#endif
BasebandSampleSink* NFMTestPlugin::createRxChannelBS(DeviceAPI *deviceAPI) const
{
return new NFMDemodTest(deviceAPI);
}
ChannelAPI* NFMTestPlugin::createRxChannelCS(DeviceAPI *deviceAPI) const
{
return new NFMDemodTest(deviceAPI);
}
ChannelWebAPIAdapter* NFMTestPlugin::createChannelWebAPIAdapter() const
{
return new NFMDemodTestWebAPIAdapter();
}

View File

@ -0,0 +1,32 @@
#ifndef INCLUDE_NFMTESTPLUGIN_H
#define INCLUDE_NFMTESTPLUGIN_H
#include <QObject>
#include "plugin/plugininterface.h"
class DeviceUISet;
class BasebandSampleSink;
class NFMTestPlugin : public QObject, PluginInterface {
Q_OBJECT
Q_INTERFACES(PluginInterface)
Q_PLUGIN_METADATA(IID "sdrangel.channel.nfmtestdemod")
public:
explicit NFMTestPlugin(QObject* parent = NULL);
const PluginDescriptor& getPluginDescriptor() const;
void initPlugin(PluginAPI* pluginAPI);
virtual PluginInstanceGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const;
virtual BasebandSampleSink* createRxChannelBS(DeviceAPI *deviceAPI) const;
virtual ChannelAPI* createRxChannelCS(DeviceAPI *deviceAPI) const;
virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const;
private:
static const PluginDescriptor m_pluginDescriptor;
PluginAPI* m_pluginAPI;
};
#endif // INCLUDE_NFMTESTPLUGIN_H

View File

@ -0,0 +1,87 @@
<h1>NFM demodulator plugin</h1>
<h2>Introduction</h2>
This plugin can be used to listen to a narrowband FM modulated signal. "Narrowband" means that the bandwidth can vary from 5 to 40 kHz.
<h2>Interface</h2>
![NFM Demodulator plugin GUI](../../../doc/img/NFMdemod_plugin.png)
<h3>1: Frequency shift from center frequency of reception value</h3>
Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. Left click on a digit sets the cursor position at this digit.
<h3>2: Channel power</h3>
Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band.
<h3>3: Level meter in dB</h3>
- top bar (green): average value
- bottom bar (blue green): instantaneous peak value
- tip vertical bar (bright green): peak hold value
<h3>4: RF bandwidth</h3>
This is the bandwidth in kHz of the channel signal before demodulation. It can be set in steps as 5, 6.25, 8.33, 10, 12.5, 15, 20, 25 and 40 kHz. The expected one side frequency deviation is 0.4 times the bandwidth.
&#9758; The demodulation is done at the channel sample rate which is guaranteed not to be lower than the requested audio sample rate but can possibly be equal to it. This means that for correct operation in any case you must ensure that the sample rate of the audio device is not lower than the Nyquist rate required to process this channel bandwidth.
&#9758; The channel sample rate is always the baseband signal rate divided by an integer power of two so depending on the baseband sample rate obtained from the sampling device you could also guarantee a minimal channel bandwidth. For example with a 125 kS/s baseband sample rate and a 8 kS/s audio sample rate the channel sample rate cannot be lower than 125/8 = 15.625 kS/s (125/16 = 7.8125 kS/s is too small) which is still OK for 5 or 6.25 kHz channel bandwidths.
<h3>5: AF bandwidth</h3>
This is the bandwidth of the audio signal in kHz (i.e. after demodulation). It can be set in continuous kHz steps from 1 to 20 kHz.
<h3>6: Volume</h3>
This is the volume of the audio signal from 0.0 (mute) to 4.0 (maximum). It can be varied continuously in 0.1 steps using the dial button.
<h3>7: Delta/Level squelch</h3>
Use this button to toggle between AF (on) and RF power (off) based squelch.
<h3>8: Squelch threshold</h3>
<h4>Power threshold mode</h4>
Case when the delta/Level squelch control (7) is off (power). This is the squelch threshold in dB. The average total power received in the signal bandwidth before demodulation is compared to this value and the squelch input is open above this value. It can be varied continuously in 1 dB steps from 0 to -100 dB using the dial button.
<h4>Audio frequency delta mode</h4>
Case when the delta/Level squelch control (7) is on (delta). In this mode the squelch compares the power of the demodulated audio signal in a low frequency band and a high frequency band. In the absence of signal the discriminator response is nearly flat and the power in the two bands is more or less balanced. In the presence of a signal the lower band will receive more power than the higher band. The squelch does the ratio of both powers and the squelch is opened if this ratio is lower than the threshold given in percent.
A ratio of 1 (100%) will always open the squelch and a ratio of 0 will always close it. The value can be varied to detect more distorted and thus weak signals towards the higher values. The button rotation runs from higher to lower as you turn it clockwise thus giving the same feel as in power mode. The best ratio for a standard NFM transmission is ~40%.
The distinct advantage of this type of squelch is that it guarantees the quality level of the audio signal (optimized for voice) thus remaining closed for too noisy signals received on marginal conditions or bursts of noise independently of the signal power.
&#9758; The signal used is the one before AF filtering and the bands are centered around 1000 Hz for the lower band and 6000 Hz for the higher band. This means that it will not work if your audio device runs at 8000 or 11025 Hz. You will need at least a 16000 Hz sample rate. Choose power squelch for lower audio rates.
&#9758; The chosen bands around 1000 and 6000 Hz are optimized for standard voice signals in the 300-3000 Hz range.
<h3>9: Squelch gate</h3>
This is the squelch gate in milliseconds. The squelch input must be open for this amount of time before the squelch actually opens. This prevents the opening of the squelch by parasitic transients. It can be varied continuously in 10ms steps from 10 to 500ms using the dial button.
<h3>10: CTCSS on/off</h3>
Use the checkbox to toggle CTCSS activation. When activated it will look for a tone squelch in the demodulated signal and display its frequency (see 10).
<h3>11: CTCSS tone</h3>
This is the tone squelch in Hz. It can be selected using the toolbox among the usual CTCSS values and `--` for none. When a value is given and the CTCSS is activated the squelch will open only for signals with this tone squelch.
<h3>12: CTCSS tone value</h3>
This is the value of the tone squelch received when the CTCSS is activated. It displays `--` if the CTCSS system is de-activated.
<h3>13: Audio high pass filter</h3>
Toggle a 300 Hz cutoff high pass filter on audio to cut-off CTCSS frequencies. It is on by default for normal audio channels usage. You can switch it off to pipe the audio in programs requiring DC like DSD+ or Multimon.
<h3>14: Audio mute and audio output select</h3>
Left click on this button to toggle audio mute for this channel. The button will light up in green if the squelch is open. This helps identifying which channels are active in a multi-channel configuration.
If you right click on it it will open a dialog to select the audio output device. See [audio management documentation](../../../sdrgui/audio.md) for details.