diff --git a/plugins/channeltx/CMakeLists.txt b/plugins/channeltx/CMakeLists.txt
index f2ea8f308..89f92144f 100644
--- a/plugins/channeltx/CMakeLists.txt
+++ b/plugins/channeltx/CMakeLists.txt
@@ -3,3 +3,4 @@ project(mod)
add_subdirectory(modam)
add_subdirectory(modnfm)
add_subdirectory(modssb)
+add_subdirectory(modwfm)
diff --git a/plugins/channeltx/modwfm/CMakeLists.txt b/plugins/channeltx/modwfm/CMakeLists.txt
new file mode 100644
index 000000000..35c64acb0
--- /dev/null
+++ b/plugins/channeltx/modwfm/CMakeLists.txt
@@ -0,0 +1,43 @@
+project(modwfm)
+
+set(modwfm_SOURCES
+ wfmmod.cpp
+ wfmmodgui.cpp
+ wfmmodplugin.cpp
+)
+
+set(modwfm_HEADERS
+ wfmmod.h
+ wfmmodgui.h
+ wfmmodplugin.h
+)
+
+set(modwfm_FORMS
+ wfmmodgui.ui
+)
+
+include_directories(
+ .
+ ${CMAKE_CURRENT_BINARY_DIR}
+)
+
+add_definitions(${QT_DEFINITIONS})
+add_definitions(-DQT_PLUGIN)
+add_definitions(-DQT_SHARED)
+
+qt5_wrap_ui(modwfm_FORMS_HEADERS ${modwfm_FORMS})
+
+add_library(modwfm SHARED
+ ${modwfm_SOURCES}
+ ${modwfm_HEADERS_MOC}
+ ${modwfm_FORMS_HEADERS}
+)
+
+target_link_libraries(modwfm
+ ${QT_LIBRARIES}
+ sdrbase
+)
+
+qt5_use_modules(modwfm Core Widgets)
+
+install(TARGETS modwfm DESTINATION lib/plugins/channeltx)
\ No newline at end of file
diff --git a/plugins/channeltx/modwfm/modwfm.pro b/plugins/channeltx/modwfm/modwfm.pro
new file mode 100644
index 000000000..c15bf2e0e
--- /dev/null
+++ b/plugins/channeltx/modwfm/modwfm.pro
@@ -0,0 +1,37 @@
+#--------------------------------------------------------
+#
+# Pro file for Android and Windows builds with Qt Creator
+#
+#--------------------------------------------------------
+
+TEMPLATE = lib
+CONFIG += plugin
+
+QT += core gui widgets multimedia
+
+TARGET = modwfm
+
+DEFINES += USE_SSE2=1
+QMAKE_CXXFLAGS += -msse2
+DEFINES += USE_SSE4_1=1
+QMAKE_CXXFLAGS += -msse4.1
+
+INCLUDEPATH += $$PWD
+INCLUDEPATH += ../../../sdrbase
+
+CONFIG(Release):build_subdir = release
+CONFIG(Debug):build_subdir = debug
+
+SOURCES += wfmmod.cpp\
+ wfmmodgui.cpp\
+ wfmmodplugin.cpp
+
+HEADERS += wfmmod.h\
+ wfmmodgui.h\
+ wfmmodplugin.h
+
+FORMS += wfmmodgui.ui
+
+LIBS += -L../../../sdrbase/$${build_subdir} -lsdrbase
+
+RESOURCES = ../../../sdrbase/resources/res.qrc
diff --git a/plugins/channeltx/modwfm/wfmmod.cpp b/plugins/channeltx/modwfm/wfmmod.cpp
new file mode 100644
index 000000000..d142045ce
--- /dev/null
+++ b/plugins/channeltx/modwfm/wfmmod.cpp
@@ -0,0 +1,427 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2016 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 //
+// //
+// 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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "dsp/dspengine.h"
+#include "dsp/pidcontroller.h"
+#include "wfmmod.h"
+
+MESSAGE_CLASS_DEFINITION(WFMMod::MsgConfigureWFMMod, Message)
+MESSAGE_CLASS_DEFINITION(WFMMod::MsgConfigureFileSourceName, Message)
+MESSAGE_CLASS_DEFINITION(WFMMod::MsgConfigureFileSourceSeek, Message)
+MESSAGE_CLASS_DEFINITION(WFMMod::MsgConfigureAFInput, Message)
+MESSAGE_CLASS_DEFINITION(WFMMod::MsgConfigureFileSourceStreamTiming, Message)
+MESSAGE_CLASS_DEFINITION(WFMMod::MsgReportFileSourceStreamData, Message)
+MESSAGE_CLASS_DEFINITION(WFMMod::MsgReportFileSourceStreamTiming, Message)
+
+const int WFMMod::m_levelNbSamples = 480; // every 10ms
+
+WFMMod::WFMMod() :
+ m_modPhasor(0.0f),
+ m_audioFifo(4, 48000),
+ m_settingsMutex(QMutex::Recursive),
+ m_fileSize(0),
+ m_recordLength(0),
+ m_sampleRate(48000),
+ m_afInput(WFMModInputNone),
+ m_levelCalcCount(0),
+ m_peakLevel(0.0f),
+ m_levelSum(0.0f)
+{
+ setObjectName("WFMod");
+
+ m_config.m_outputSampleRate = 384000;
+ m_config.m_inputFrequencyOffset = 0;
+ m_config.m_rfBandwidth = 125000;
+ m_config.m_afBandwidth = 8000;
+ m_config.m_fmDeviation = 50000.0f;
+ m_config.m_toneFrequency = 1000.0f;
+ m_config.m_audioSampleRate = DSPEngine::instance()->getAudioSampleRate();
+
+ apply();
+
+ //m_audioBuffer.resize(1<<14);
+ //m_audioBufferFill = 0;
+
+ m_movingAverage.resize(16, 0);
+ m_volumeAGC.resize(4096, 0.003, 0);
+ m_magsq = 0.0;
+
+ m_toneNco.setFreq(1000.0, m_config.m_audioSampleRate);
+ DSPEngine::instance()->addAudioSource(&m_audioFifo);
+
+ // CW keyer
+ m_cwKeyer.setSampleRate(m_config.m_audioSampleRate);
+ m_cwKeyer.setWPM(13);
+ m_cwKeyer.setMode(CWKeyer::CWNone);
+ m_cwSmoother.setNbFadeSamples(192); // 2 ms @ 48 kHz
+}
+
+WFMMod::~WFMMod()
+{
+ DSPEngine::instance()->removeAudioSource(&m_audioFifo);
+}
+
+void WFMMod::configure(MessageQueue* messageQueue,
+ Real rfBandwidth,
+ Real afBandwidth,
+ float fmDeviation,
+ float toneFrequency,
+ float volumeFactor,
+ bool audioMute,
+ bool playLoop)
+{
+ Message* cmd = MsgConfigureWFMMod::create(rfBandwidth, afBandwidth, fmDeviation, toneFrequency, volumeFactor, audioMute, playLoop);
+ messageQueue->push(cmd);
+}
+
+void WFMMod::pull(Sample& sample)
+{
+ Complex ci;
+ Real t;
+
+ m_settingsMutex.lock();
+
+ if (m_interpolatorDistance > 1.0f) // decimate
+ {
+ modulateSample();
+
+ while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci))
+ {
+ modulateSample();
+ }
+ }
+ else
+ {
+ if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci))
+ {
+ modulateSample();
+ }
+ }
+
+ m_interpolatorDistanceRemain += m_interpolatorDistance;
+
+ ci *= m_carrierNco.nextIQ(); // shift to carrier frequency
+
+ m_settingsMutex.unlock();
+
+ Real magsq = ci.real() * ci.real() + ci.imag() * ci.imag();
+ magsq /= (1<<30);
+ m_movingAverage.feed(magsq);
+ m_magsq = m_movingAverage.average();
+
+ sample.m_real = (FixReal) ci.real();
+ sample.m_imag = (FixReal) ci.imag();
+}
+
+void WFMMod::modulateSample()
+{
+ Real t;
+
+ pullAF(t);
+ calculateLevel(t);
+
+ // 378 = 302 * 1.25; 302 = number of filter taps (established experimentally)
+ m_modPhasor += (m_running.m_fmDeviation / (float) m_running.m_audioSampleRate) * m_bandpass.filter(t) * (M_PI / 378.0f);
+ m_modSample.real(cos(m_modPhasor) * 32678.0f);
+ m_modSample.imag(sin(m_modPhasor) * 32678.0f);
+}
+
+void WFMMod::pullAF(Real& sample)
+{
+ int16_t audioSample[2];
+
+ switch (m_afInput)
+ {
+ case WFMModInputTone:
+ sample = m_toneNco.next();
+ break;
+ case WFMModInputFile:
+ // sox f4exb_call.wav --encoding float --endian little f4exb_call.raw
+ // ffplay -f f32le -ar 48k -ac 1 f4exb_call.raw
+ if (m_ifstream.is_open())
+ {
+ if (m_ifstream.eof())
+ {
+ if (m_running.m_playLoop)
+ {
+ m_ifstream.clear();
+ m_ifstream.seekg(0, std::ios::beg);
+ }
+ }
+
+ if (m_ifstream.eof())
+ {
+ sample = 0.0f;
+ }
+ else
+ {
+ m_ifstream.read(reinterpret_cast(&sample), sizeof(Real));
+ sample *= m_running.m_volumeFactor;
+ }
+ }
+ else
+ {
+ sample = 0.0f;
+ }
+ break;
+ case WFMModInputAudio:
+ m_audioFifo.read(reinterpret_cast(audioSample), 1, 10);
+ sample = ((audioSample[0] + audioSample[1]) / 65536.0f) * m_running.m_volumeFactor;
+ break;
+ case WFMModInputCWTone:
+ Real fadeFactor;
+
+ if (m_cwKeyer.getSample())
+ {
+ m_cwSmoother.getFadeSample(true, fadeFactor);
+ sample = m_toneNco.next() * fadeFactor;
+ }
+ else
+ {
+ if (m_cwSmoother.getFadeSample(false, fadeFactor))
+ {
+ sample = m_toneNco.next() * fadeFactor;
+ }
+ else
+ {
+ sample = 0.0f;
+ m_toneNco.setPhase(0);
+ }
+ }
+ break;
+ case WFMModInputNone:
+ default:
+ sample = 0.0f;
+ break;
+ }
+}
+
+void WFMMod::calculateLevel(Real& sample)
+{
+ if (m_levelCalcCount < m_levelNbSamples)
+ {
+ m_peakLevel = std::max(std::fabs(m_peakLevel), sample);
+ m_levelSum += sample * sample;
+ m_levelCalcCount++;
+ }
+ else
+ {
+ qreal rmsLevel = sqrt(m_levelSum / m_levelNbSamples);
+ //qDebug("WFMMod::calculateLevel: %f %f", rmsLevel, m_peakLevel);
+ emit levelChanged(rmsLevel, m_peakLevel, m_levelNbSamples);
+ m_peakLevel = 0.0f;
+ m_levelSum = 0.0f;
+ m_levelCalcCount = 0;
+ }
+}
+
+void WFMMod::start()
+{
+ qDebug() << "WFMMod::start: m_outputSampleRate: " << m_config.m_outputSampleRate
+ << " m_inputFrequencyOffset: " << m_config.m_inputFrequencyOffset;
+
+ m_audioFifo.clear();
+}
+
+void WFMMod::stop()
+{
+}
+
+bool WFMMod::handleMessage(const Message& cmd)
+{
+ qDebug() << "WFMMod::handleMessage";
+
+ if (UpChannelizer::MsgChannelizerNotification::match(cmd))
+ {
+ UpChannelizer::MsgChannelizerNotification& notif = (UpChannelizer::MsgChannelizerNotification&) cmd;
+
+ m_config.m_outputSampleRate = notif.getSampleRate();
+ m_config.m_inputFrequencyOffset = notif.getFrequencyOffset();
+
+ apply();
+
+ qDebug() << "WFMMod::handleMessage: MsgChannelizerNotification:"
+ << " m_outputSampleRate: " << m_config.m_outputSampleRate
+ << " m_inputFrequencyOffset: " << m_config.m_inputFrequencyOffset;
+
+ return true;
+ }
+ else if (MsgConfigureWFMMod::match(cmd))
+ {
+ MsgConfigureWFMMod& cfg = (MsgConfigureWFMMod&) cmd;
+
+ m_config.m_rfBandwidth = cfg.getRFBandwidth();
+ m_config.m_afBandwidth = cfg.getAFBandwidth();
+ m_config.m_fmDeviation = cfg.getFMDeviation();
+ m_config.m_toneFrequency = cfg.getToneFrequency();
+ m_config.m_volumeFactor = cfg.getVolumeFactor();
+ m_config.m_audioMute = cfg.getAudioMute();
+ m_config.m_playLoop = cfg.getPlayLoop();
+
+ apply();
+
+ qDebug() << "WFMMod::handleMessage: MsgConfigureWFMMod:"
+ << " m_rfBandwidth: " << m_config.m_rfBandwidth
+ << " m_afBandwidth: " << m_config.m_afBandwidth
+ << " m_fmDeviation: " << m_config.m_fmDeviation
+ << " m_toneFrequency: " << m_config.m_toneFrequency
+ << " m_volumeFactor: " << m_config.m_volumeFactor
+ << " m_audioMute: " << m_config.m_audioMute
+ << " m_playLoop: " << m_config.m_playLoop;
+
+ return true;
+ }
+ else if (MsgConfigureFileSourceName::match(cmd))
+ {
+ MsgConfigureFileSourceName& conf = (MsgConfigureFileSourceName&) cmd;
+ m_fileName = conf.getFileName();
+ openFileStream();
+ return true;
+ }
+ else if (MsgConfigureFileSourceSeek::match(cmd))
+ {
+ MsgConfigureFileSourceSeek& conf = (MsgConfigureFileSourceSeek&) cmd;
+ int seekPercentage = conf.getPercentage();
+ seekFileStream(seekPercentage);
+
+ return true;
+ }
+ else if (MsgConfigureAFInput::match(cmd))
+ {
+ MsgConfigureAFInput& conf = (MsgConfigureAFInput&) cmd;
+ m_afInput = conf.getAFInput();
+
+ return true;
+ }
+ else if (MsgConfigureFileSourceStreamTiming::match(cmd))
+ {
+ std::size_t samplesCount;
+
+ if (m_ifstream.eof()) {
+ samplesCount = m_fileSize / sizeof(Real);
+ } else {
+ samplesCount = m_ifstream.tellg() / sizeof(Real);
+ }
+
+ MsgReportFileSourceStreamTiming *report;
+ report = MsgReportFileSourceStreamTiming::create(samplesCount);
+ getOutputMessageQueue()->push(report);
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+void WFMMod::apply()
+{
+
+ if ((m_config.m_inputFrequencyOffset != m_running.m_inputFrequencyOffset) ||
+ (m_config.m_outputSampleRate != m_running.m_outputSampleRate))
+ {
+ m_settingsMutex.lock();
+ m_carrierNco.setFreq(m_config.m_inputFrequencyOffset, m_config.m_outputSampleRate);
+ m_settingsMutex.unlock();
+ }
+
+ if((m_config.m_outputSampleRate != m_running.m_outputSampleRate) ||
+ (m_config.m_rfBandwidth != m_running.m_rfBandwidth))
+ {
+ m_settingsMutex.lock();
+ m_interpolatorDistanceRemain = 0;
+ m_interpolatorConsumed = false;
+ m_interpolatorDistance = (Real) m_config.m_audioSampleRate / (Real) m_config.m_outputSampleRate;
+ m_interpolator.create(48, m_config.m_audioSampleRate, m_config.m_rfBandwidth / 2.2, 3.0);
+ m_settingsMutex.unlock();
+ }
+
+ if ((m_config.m_afBandwidth != m_running.m_afBandwidth) ||
+ (m_config.m_audioSampleRate != m_running.m_audioSampleRate))
+ {
+ m_settingsMutex.lock();
+ m_lowpass.create(301, m_config.m_audioSampleRate, 250.0);
+ m_bandpass.create(301, m_config.m_audioSampleRate, 300.0, m_config.m_afBandwidth);
+ m_settingsMutex.unlock();
+ }
+
+ if ((m_config.m_toneFrequency != m_running.m_toneFrequency) ||
+ (m_config.m_audioSampleRate != m_running.m_audioSampleRate))
+ {
+ m_settingsMutex.lock();
+ m_toneNco.setFreq(m_config.m_toneFrequency, m_config.m_audioSampleRate);
+ m_settingsMutex.unlock();
+ }
+
+ if (m_config.m_audioSampleRate != m_running.m_audioSampleRate)
+ {
+ m_cwKeyer.setSampleRate(m_config.m_audioSampleRate);
+ m_cwSmoother.setNbFadeSamples(m_config.m_audioSampleRate / 250); // 4 ms
+ }
+
+ m_running.m_outputSampleRate = m_config.m_outputSampleRate;
+ m_running.m_inputFrequencyOffset = m_config.m_inputFrequencyOffset;
+ m_running.m_rfBandwidth = m_config.m_rfBandwidth;
+ m_running.m_afBandwidth = m_config.m_afBandwidth;
+ m_running.m_fmDeviation = m_config.m_fmDeviation;
+ m_running.m_volumeFactor = m_config.m_volumeFactor;
+ m_running.m_audioSampleRate = m_config.m_audioSampleRate;
+ m_running.m_audioMute = m_config.m_audioMute;
+ m_running.m_playLoop = m_config.m_playLoop;
+}
+
+void WFMMod::openFileStream()
+{
+ if (m_ifstream.is_open()) {
+ m_ifstream.close();
+ }
+
+ m_ifstream.open(m_fileName.toStdString().c_str(), std::ios::binary | std::ios::ate);
+ m_fileSize = m_ifstream.tellg();
+ m_ifstream.seekg(0,std::ios_base::beg);
+
+ m_sampleRate = 48000; // fixed rate
+ m_recordLength = m_fileSize / (sizeof(Real) * m_sampleRate);
+
+ qDebug() << "WFMMod::openFileStream: " << m_fileName.toStdString().c_str()
+ << " fileSize: " << m_fileSize << "bytes"
+ << " length: " << m_recordLength << " seconds";
+
+ MsgReportFileSourceStreamData *report;
+ report = MsgReportFileSourceStreamData::create(m_sampleRate, m_recordLength);
+ getOutputMessageQueue()->push(report);
+}
+
+void WFMMod::seekFileStream(int seekPercentage)
+{
+ QMutexLocker mutexLocker(&m_settingsMutex);
+
+ if (m_ifstream.is_open())
+ {
+ int seekPoint = ((m_recordLength * seekPercentage) / 100) * m_sampleRate;
+ seekPoint *= sizeof(Real);
+ m_ifstream.clear();
+ m_ifstream.seekg(seekPoint, std::ios::beg);
+ }
+}
diff --git a/plugins/channeltx/modwfm/wfmmod.h b/plugins/channeltx/modwfm/wfmmod.h
new file mode 100644
index 000000000..04f795755
--- /dev/null
+++ b/plugins/channeltx/modwfm/wfmmod.h
@@ -0,0 +1,337 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2016 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 //
+// //
+// 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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef PLUGINS_CHANNELTX_MODWFM_WFMMOD_H_
+#define PLUGINS_CHANNELTX_MODWFM_WFMMOD_H_
+
+#include
+#include
+#include
+#include
+
+#include "dsp/basebandsamplesource.h"
+#include "dsp/nco.h"
+#include "dsp/interpolator.h"
+#include "dsp/lowpass.h"
+#include "dsp/bandpass.h"
+#include "dsp/movingaverage.h"
+#include "dsp/agc.h"
+#include "dsp/cwkeyer.h"
+#include "audio/audiofifo.h"
+#include "util/message.h"
+
+class WFMMod : public BasebandSampleSource {
+ Q_OBJECT
+
+public:
+ typedef enum
+ {
+ WFMModInputNone,
+ WFMModInputTone,
+ WFMModInputFile,
+ WFMModInputAudio,
+ WFMModInputCWTone
+ } WFMModInputAF;
+
+ class MsgConfigureFileSourceName : public Message
+ {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+ const QString& getFileName() const { return m_fileName; }
+
+ static MsgConfigureFileSourceName* create(const QString& fileName)
+ {
+ return new MsgConfigureFileSourceName(fileName);
+ }
+
+ private:
+ QString m_fileName;
+
+ MsgConfigureFileSourceName(const QString& fileName) :
+ Message(),
+ m_fileName(fileName)
+ { }
+ };
+
+ class MsgConfigureFileSourceSeek : public Message
+ {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+ int getPercentage() const { return m_seekPercentage; }
+
+ static MsgConfigureFileSourceSeek* create(int seekPercentage)
+ {
+ return new MsgConfigureFileSourceSeek(seekPercentage);
+ }
+
+ protected:
+ int m_seekPercentage; //!< percentage of seek position from the beginning 0..100
+
+ MsgConfigureFileSourceSeek(int seekPercentage) :
+ Message(),
+ m_seekPercentage(seekPercentage)
+ { }
+ };
+
+ class MsgConfigureFileSourceStreamTiming : public Message {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+
+ static MsgConfigureFileSourceStreamTiming* create()
+ {
+ return new MsgConfigureFileSourceStreamTiming();
+ }
+
+ private:
+
+ MsgConfigureFileSourceStreamTiming() :
+ Message()
+ { }
+ };
+
+ class MsgConfigureAFInput : public Message
+ {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+ WFMModInputAF getAFInput() const { return m_afInput; }
+
+ static MsgConfigureAFInput* create(WFMModInputAF afInput)
+ {
+ return new MsgConfigureAFInput(afInput);
+ }
+
+ private:
+ WFMModInputAF m_afInput;
+
+ MsgConfigureAFInput(WFMModInputAF afInput) :
+ Message(),
+ m_afInput(afInput)
+ { }
+ };
+
+ class MsgReportFileSourceStreamTiming : public Message
+ {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+ std::size_t getSamplesCount() const { return m_samplesCount; }
+
+ static MsgReportFileSourceStreamTiming* create(std::size_t samplesCount)
+ {
+ return new MsgReportFileSourceStreamTiming(samplesCount);
+ }
+
+ protected:
+ std::size_t m_samplesCount;
+
+ MsgReportFileSourceStreamTiming(std::size_t samplesCount) :
+ Message(),
+ m_samplesCount(samplesCount)
+ { }
+ };
+
+ class MsgReportFileSourceStreamData : public Message {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+ int getSampleRate() const { return m_sampleRate; }
+ quint32 getRecordLength() const { return m_recordLength; }
+
+ static MsgReportFileSourceStreamData* create(int sampleRate,
+ quint32 recordLength)
+ {
+ return new MsgReportFileSourceStreamData(sampleRate, recordLength);
+ }
+
+ protected:
+ int m_sampleRate;
+ quint32 m_recordLength;
+
+ MsgReportFileSourceStreamData(int sampleRate,
+ quint32 recordLength) :
+ Message(),
+ m_sampleRate(sampleRate),
+ m_recordLength(recordLength)
+ { }
+ };
+
+ //=================================================================
+
+ WFMMod();
+ ~WFMMod();
+
+ void configure(MessageQueue* messageQueue,
+ Real rfBandwidth,
+ Real afBandwidth,
+ float fmDeviation,
+ float toneFrequency,
+ float volumeFactor,
+ bool audioMute,
+ bool playLoop);
+
+ virtual void pull(Sample& sample);
+ virtual void start();
+ virtual void stop();
+ virtual bool handleMessage(const Message& cmd);
+
+ Real getMagSq() const { return m_magsq; }
+
+ CWKeyer *getCWKeyer() { return &m_cwKeyer; }
+
+signals:
+ /**
+ * Level changed
+ * \param rmsLevel RMS level in range 0.0 - 1.0
+ * \param peakLevel Peak level in range 0.0 - 1.0
+ * \param numSamples Number of audio samples analyzed
+ */
+ void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples);
+
+
+private:
+ class MsgConfigureWFMMod : public Message
+ {
+ MESSAGE_CLASS_DECLARATION
+
+ public:
+ Real getRFBandwidth() const { return m_rfBandwidth; }
+ Real getAFBandwidth() const { return m_afBandwidth; }
+ float getFMDeviation() const { return m_fmDeviation; }
+ float getToneFrequency() const { return m_toneFrequency; }
+ float getVolumeFactor() const { return m_volumeFactor; }
+ bool getAudioMute() const { return m_audioMute; }
+ bool getPlayLoop() const { return m_playLoop; }
+
+ static MsgConfigureWFMMod* create(Real rfBandwidth, Real afBandwidth, float fmDeviation, float toneFrequency, int volumeFactor, bool audioMute, bool playLoop)
+ {
+ return new MsgConfigureWFMMod(rfBandwidth, afBandwidth, fmDeviation, toneFrequency, volumeFactor, audioMute, playLoop);
+ }
+
+ private:
+ Real m_rfBandwidth;
+ Real m_afBandwidth;
+ float m_fmDeviation;
+ float m_toneFrequency;
+ float m_volumeFactor;
+ bool m_audioMute;
+ bool m_playLoop;
+
+ MsgConfigureWFMMod(Real rfBandwidth, Real afBandwidth, float fmDeviation, float toneFrequency, float volumeFactor, bool audioMute, bool playLoop) :
+ Message(),
+ m_rfBandwidth(rfBandwidth),
+ m_afBandwidth(afBandwidth),
+ m_fmDeviation(fmDeviation),
+ m_toneFrequency(toneFrequency),
+ m_volumeFactor(volumeFactor),
+ m_audioMute(audioMute),
+ m_playLoop(playLoop)
+ { }
+ };
+
+ //=================================================================
+
+ struct AudioSample {
+ qint16 l;
+ qint16 r;
+ };
+ typedef std::vector AudioVector;
+
+ enum RateState {
+ RSInitialFill,
+ RSRunning
+ };
+
+ struct Config {
+ int m_outputSampleRate;
+ qint64 m_inputFrequencyOffset;
+ Real m_rfBandwidth;
+ Real m_afBandwidth;
+ float m_fmDeviation;
+ float m_toneFrequency;
+ float m_volumeFactor;
+ quint32 m_audioSampleRate;
+ bool m_audioMute;
+ bool m_playLoop;
+
+ Config() :
+ m_outputSampleRate(-1),
+ m_inputFrequencyOffset(0),
+ m_rfBandwidth(-1),
+ m_afBandwidth(-1),
+ m_fmDeviation(5000.0f),
+ m_toneFrequency(1000.0f),
+ m_volumeFactor(1.0f),
+ m_audioSampleRate(0),
+ m_audioMute(false),
+ m_playLoop(false)
+ { }
+ };
+
+ //=================================================================
+
+ Config m_config;
+ Config m_running;
+
+ NCO m_carrierNco;
+ NCO m_toneNco;
+ float m_modPhasor; //!< baseband modulator phasor
+ Complex m_modSample;
+ Interpolator m_interpolator;
+ Real m_interpolatorDistance;
+ Real m_interpolatorDistanceRemain;
+ bool m_interpolatorConsumed;
+ Lowpass m_lowpass;
+ Bandpass m_bandpass;
+
+ Real m_magsq;
+ MovingAverage m_movingAverage;
+ SimpleAGC m_volumeAGC;
+
+ //AudioVector m_audioBuffer;
+ //uint m_audioBufferFill;
+
+ AudioFifo m_audioFifo;
+ SampleVector m_sampleBuffer;
+ QMutex m_settingsMutex;
+
+ std::ifstream m_ifstream;
+ QString m_fileName;
+ quint64 m_fileSize; //!< raw file size (bytes)
+ quint32 m_recordLength; //!< record length in seconds computed from file size
+ int m_sampleRate;
+
+ WFMModInputAF m_afInput;
+ quint32 m_levelCalcCount;
+ Real m_peakLevel;
+ Real m_levelSum;
+ CWKeyer m_cwKeyer;
+ CWSmoother m_cwSmoother;
+ static const int m_levelNbSamples;
+
+ void apply();
+ void pullAF(Real& sample);
+ void calculateLevel(Real& sample);
+ void modulateSample();
+ void openFileStream();
+ void seekFileStream(int seekPercentage);
+};
+
+
+#endif /* PLUGINS_CHANNELTX_MODWFM_WFMMOD_H_ */
diff --git a/plugins/channeltx/modwfm/wfmmodgui.cpp b/plugins/channeltx/modwfm/wfmmodgui.cpp
new file mode 100644
index 000000000..9e4d40778
--- /dev/null
+++ b/plugins/channeltx/modwfm/wfmmodgui.cpp
@@ -0,0 +1,506 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2016 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 //
+// //
+// 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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include
+#include
+#include
+
+#include "device/devicesinkapi.h"
+#include "dsp/upchannelizer.h"
+
+#include "dsp/threadedbasebandsamplesource.h"
+#include "ui_wfmmodgui.h"
+#include "plugin/pluginapi.h"
+#include "util/simpleserializer.h"
+#include "util/db.h"
+#include "gui/basicchannelsettingswidget.h"
+#include "dsp/dspengine.h"
+#include "mainwindow.h"
+#include "wfmmodgui.h"
+
+const QString WFMModGUI::m_channelID = "sdrangel.channeltx.modwfm";
+
+const int WFMModGUI::m_rfBW[] = {
+ 12500, 25000, 40000, 60000, 75000, 80000, 100000, 125000, 140000, 160000, 180000, 200000, 220000, 250000
+};
+const int WFMModGUI::m_nbRfBW = 14;
+
+WFMModGUI* WFMModGUI::create(PluginAPI* pluginAPI, DeviceSinkAPI *deviceAPI)
+{
+ WFMModGUI* gui = new WFMModGUI(pluginAPI, deviceAPI);
+ return gui;
+}
+
+void WFMModGUI::destroy()
+{
+}
+
+void WFMModGUI::setName(const QString& name)
+{
+ setObjectName(name);
+}
+
+QString WFMModGUI::getName() const
+{
+ return objectName();
+}
+
+qint64 WFMModGUI::getCenterFrequency() const {
+ return m_channelMarker.getCenterFrequency();
+}
+
+void WFMModGUI::setCenterFrequency(qint64 centerFrequency)
+{
+ m_channelMarker.setCenterFrequency(centerFrequency);
+ applySettings();
+}
+
+void WFMModGUI::resetToDefaults()
+{
+ blockApplySettings(true);
+
+ ui->rfBW->setCurrentIndex(7);
+ ui->afBW->setValue(8);
+ ui->fmDev->setValue(50);
+ ui->toneFrequency->setValue(100);
+ ui->volume->setValue(10);
+ ui->deltaFrequency->setValue(0);
+
+ blockApplySettings(false);
+ applySettings();
+}
+
+QByteArray WFMModGUI::serialize() const
+{
+ SimpleSerializer s(1);
+ s.writeS32(1, m_channelMarker.getCenterFrequency());
+ s.writeS32(2, ui->rfBW->currentIndex());
+ s.writeS32(3, ui->afBW->value());
+ s.writeS32(4, ui->fmDev->value());
+ s.writeU32(5, m_channelMarker.getColor().rgb());
+ s.writeS32(6, ui->toneFrequency->value());
+ s.writeS32(7, ui->volume->value());
+ return s.final();
+}
+
+bool WFMModGUI::deserialize(const QByteArray& data)
+{
+ SimpleDeserializer d(data);
+
+ if(!d.isValid())
+ {
+ resetToDefaults();
+ return false;
+ }
+
+ if(d.getVersion() == 1)
+ {
+ QByteArray bytetmp;
+ quint32 u32tmp;
+ qint32 tmp;
+
+ blockApplySettings(true);
+ m_channelMarker.blockSignals(true);
+
+ d.readS32(1, &tmp, 0);
+ m_channelMarker.setCenterFrequency(tmp);
+ d.readS32(2, &tmp, 6);
+ ui->rfBW->setCurrentIndex(tmp);
+ d.readS32(3, &tmp, 3);
+ ui->afBW->setValue(tmp);
+ d.readS32(4, &tmp, 50);
+ ui->fmDev->setValue(tmp);
+
+ if(d.readU32(5, &u32tmp))
+ {
+ m_channelMarker.setColor(u32tmp);
+ }
+
+ d.readS32(6, &tmp, 100);
+ ui->toneFrequency->setValue(tmp);
+ d.readS32(7, &tmp, 10);
+ ui->volume->setValue(tmp);
+
+ blockApplySettings(false);
+ m_channelMarker.blockSignals(false);
+
+ applySettings();
+ return true;
+ }
+ else
+ {
+ resetToDefaults();
+ return false;
+ }
+}
+
+bool WFMModGUI::handleMessage(const Message& message)
+{
+ if (WFMMod::MsgReportFileSourceStreamData::match(message))
+ {
+ m_recordSampleRate = ((WFMMod::MsgReportFileSourceStreamData&)message).getSampleRate();
+ m_recordLength = ((WFMMod::MsgReportFileSourceStreamData&)message).getRecordLength();
+ m_samplesCount = 0;
+ updateWithStreamData();
+ return true;
+ }
+ else if (WFMMod::MsgReportFileSourceStreamTiming::match(message))
+ {
+ m_samplesCount = ((WFMMod::MsgReportFileSourceStreamTiming&)message).getSamplesCount();
+ updateWithStreamTime();
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+void WFMModGUI::viewChanged()
+{
+ applySettings();
+}
+
+void WFMModGUI::handleSourceMessages()
+{
+ Message* message;
+
+ while ((message = m_wfmMod->getOutputMessageQueue()->pop()) != 0)
+ {
+ if (handleMessage(*message))
+ {
+ delete message;
+ }
+ }
+}
+
+void WFMModGUI::on_deltaMinus_toggled(bool minus)
+{
+ int deltaFrequency = m_channelMarker.getCenterFrequency();
+ bool minusDelta = (deltaFrequency < 0);
+
+ if (minus ^ minusDelta) // sign change
+ {
+ m_channelMarker.setCenterFrequency(-deltaFrequency);
+ }
+}
+
+void WFMModGUI::on_deltaFrequency_changed(quint64 value)
+{
+ if (ui->deltaMinus->isChecked()) {
+ m_channelMarker.setCenterFrequency(-value);
+ } else {
+ m_channelMarker.setCenterFrequency(value);
+ }
+}
+
+void WFMModGUI::on_rfBW_currentIndexChanged(int index)
+{
+ m_channelMarker.setBandwidth(m_rfBW[index]);
+ applySettings();
+}
+
+void WFMModGUI::on_afBW_valueChanged(int value)
+{
+ ui->afBWText->setText(QString("%1k").arg(value));
+ applySettings();
+}
+
+void WFMModGUI::on_fmDev_valueChanged(int value)
+{
+ ui->fmDevText->setText(QString("%1k").arg(value));
+ applySettings();
+}
+
+void WFMModGUI::on_volume_valueChanged(int value)
+{
+ ui->volumeText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1));
+ applySettings();
+}
+
+void WFMModGUI::on_toneFrequency_valueChanged(int value)
+{
+ ui->toneFrequencyText->setText(QString("%1k").arg(value / 100.0, 0, 'f', 2));
+ applySettings();
+}
+
+void WFMModGUI::on_audioMute_toggled(bool checked)
+{
+ applySettings();
+}
+
+void WFMModGUI::on_playLoop_toggled(bool checked)
+{
+ applySettings();
+}
+
+void WFMModGUI::on_play_toggled(bool checked)
+{
+ ui->tone->setEnabled(!checked); // release other source inputs
+ ui->mic->setEnabled(!checked);
+ ui->morseKeyer->setEnabled(!checked);
+ m_modAFInput = checked ? WFMMod::WFMModInputFile : WFMMod::WFMModInputNone;
+ WFMMod::MsgConfigureAFInput* message = WFMMod::MsgConfigureAFInput::create(m_modAFInput);
+ m_wfmMod->getInputMessageQueue()->push(message);
+ ui->navTimeSlider->setEnabled(!checked);
+ m_enableNavTime = !checked;
+}
+
+void WFMModGUI::on_tone_toggled(bool checked)
+{
+ ui->play->setEnabled(!checked); // release other source inputs
+ ui->mic->setEnabled(!checked);
+ ui->morseKeyer->setEnabled(!checked);
+ m_modAFInput = checked ? WFMMod::WFMModInputTone : WFMMod::WFMModInputNone;
+ WFMMod::MsgConfigureAFInput* message = WFMMod::MsgConfigureAFInput::create(m_modAFInput);
+ m_wfmMod->getInputMessageQueue()->push(message);
+}
+
+void WFMModGUI::on_morseKeyer_toggled(bool checked)
+{
+ ui->tone->setEnabled(!checked); // release other source inputs
+ ui->mic->setEnabled(!checked);
+ ui->play->setEnabled(!checked);
+ m_modAFInput = checked ? WFMMod::WFMModInputCWTone : WFMMod::WFMModInputNone;
+ WFMMod::MsgConfigureAFInput* message = WFMMod::MsgConfigureAFInput::create(m_modAFInput);
+ m_wfmMod->getInputMessageQueue()->push(message);
+}
+
+void WFMModGUI::on_mic_toggled(bool checked)
+{
+ ui->play->setEnabled(!checked); // release other source inputs
+ ui->tone->setEnabled(!checked); // release other source inputs
+ ui->morseKeyer->setEnabled(!checked);
+ m_modAFInput = checked ? WFMMod::WFMModInputAudio : WFMMod::WFMModInputNone;
+ WFMMod::MsgConfigureAFInput* message = WFMMod::MsgConfigureAFInput::create(m_modAFInput);
+ m_wfmMod->getInputMessageQueue()->push(message);
+}
+
+void WFMModGUI::on_navTimeSlider_valueChanged(int value)
+{
+ if (m_enableNavTime && ((value >= 0) && (value <= 100)))
+ {
+ int t_sec = (m_recordLength * value) / 100;
+ QTime t(0, 0, 0, 0);
+ t = t.addSecs(t_sec);
+
+ WFMMod::MsgConfigureFileSourceSeek* message = WFMMod::MsgConfigureFileSourceSeek::create(value);
+ m_wfmMod->getInputMessageQueue()->push(message);
+ }
+}
+
+void WFMModGUI::on_showFileDialog_clicked(bool checked)
+{
+ QString fileName = QFileDialog::getOpenFileName(this,
+ tr("Open raw audio file"), ".", tr("Raw audio Files (*.raw)"));
+
+ if (fileName != "")
+ {
+ m_fileName = fileName;
+ ui->recordFileText->setText(m_fileName);
+ ui->play->setEnabled(true);
+ configureFileName();
+ }
+}
+
+void WFMModGUI::configureFileName()
+{
+ qDebug() << "FileSourceGui::configureFileName: " << m_fileName.toStdString().c_str();
+ WFMMod::MsgConfigureFileSourceName* message = WFMMod::MsgConfigureFileSourceName::create(m_fileName);
+ m_wfmMod->getInputMessageQueue()->push(message);
+}
+
+void WFMModGUI::onWidgetRolled(QWidget* widget, bool rollDown)
+{
+}
+
+void WFMModGUI::onMenuDoubleClicked()
+{
+ if(!m_basicSettingsShown) {
+ m_basicSettingsShown = true;
+ BasicChannelSettingsWidget* bcsw = new BasicChannelSettingsWidget(&m_channelMarker, this);
+ bcsw->show();
+ }
+}
+
+WFMModGUI::WFMModGUI(PluginAPI* pluginAPI, DeviceSinkAPI *deviceAPI, QWidget* parent) :
+ RollupWidget(parent),
+ ui(new Ui::WFMModGUI),
+ m_pluginAPI(pluginAPI),
+ m_deviceAPI(deviceAPI),
+ m_channelMarker(this),
+ m_basicSettingsShown(false),
+ m_doApplySettings(true),
+ m_channelPowerDbAvg(20,0),
+ m_recordLength(0),
+ m_recordSampleRate(48000),
+ m_samplesCount(0),
+ m_tickCount(0),
+ m_enableNavTime(false),
+ m_modAFInput(WFMMod::WFMModInputNone)
+{
+ ui->setupUi(this);
+ setAttribute(Qt::WA_DeleteOnClose, true);
+
+ blockApplySettings(true);
+ ui->rfBW->clear();
+ for (int i = 0; i < m_nbRfBW; i++) {
+ ui->rfBW->addItem(QString("%1").arg(m_rfBW[i] / 1000.0, 0, 'f', 2));
+ }
+ ui->rfBW->setCurrentIndex(6);
+ blockApplySettings(false);
+
+
+ connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
+ connect(this, SIGNAL(menuDoubleClickEvent()), this, SLOT(onMenuDoubleClicked()));
+
+ m_wfmMod = new WFMMod();
+ m_channelizer = new UpChannelizer(m_wfmMod);
+ m_threadedChannelizer = new ThreadedBasebandSampleSource(m_channelizer, this);
+ //m_pluginAPI->addThreadedSink(m_threadedChannelizer);
+ m_deviceAPI->addThreadedSource(m_threadedChannelizer);
+
+ connect(&m_pluginAPI->getMainWindow()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick()));
+
+ ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::ReverseGold));
+
+ //m_channelMarker = new ChannelMarker(this);
+ m_channelMarker.setColor(Qt::blue);
+ m_channelMarker.setBandwidth(12500);
+ m_channelMarker.setCenterFrequency(0);
+ m_channelMarker.setVisible(true);
+
+ connect(&m_channelMarker, SIGNAL(changed()), this, SLOT(viewChanged()));
+
+ m_deviceAPI->registerChannelInstance(m_channelID, this);
+ m_deviceAPI->addChannelMarker(&m_channelMarker);
+ m_deviceAPI->addRollupWidget(this);
+
+ ui->play->setEnabled(false);
+ ui->play->setChecked(false);
+ ui->tone->setChecked(false);
+ ui->mic->setChecked(false);
+
+ ui->cwKeyerGUI->setBuddies(m_wfmMod->getInputMessageQueue(), m_wfmMod->getCWKeyer());
+
+ applySettings();
+
+ connect(m_wfmMod->getOutputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages()));
+ connect(m_wfmMod, SIGNAL(levelChanged(qreal, qreal, int)), ui->volumeMeter, SLOT(levelChanged(qreal, qreal, int)));
+}
+
+WFMModGUI::~WFMModGUI()
+{
+ m_deviceAPI->removeChannelInstance(this);
+ m_deviceAPI->removeThreadedSource(m_threadedChannelizer);
+ delete m_threadedChannelizer;
+ delete m_channelizer;
+ delete m_wfmMod;
+ //delete m_channelMarker;
+ delete ui;
+}
+
+void WFMModGUI::blockApplySettings(bool block)
+{
+ m_doApplySettings = !block;
+}
+
+void WFMModGUI::applySettings()
+{
+ if (m_doApplySettings)
+ {
+ setTitleColor(m_channelMarker.getColor());
+
+ m_channelizer->configure(m_channelizer->getInputMessageQueue(),
+ 48000,
+ m_channelMarker.getCenterFrequency());
+
+ ui->deltaFrequency->setValue(abs(m_channelMarker.getCenterFrequency()));
+ ui->deltaMinus->setChecked(m_channelMarker.getCenterFrequency() < 0);
+
+ m_wfmMod->configure(m_wfmMod->getInputMessageQueue(),
+ m_rfBW[ui->rfBW->currentIndex()],
+ ui->afBW->value() * 1000.0,
+ ui->fmDev->value() * 100.0f, // value is in '100 Hz
+ ui->toneFrequency->value() * 10.0f,
+ ui->volume->value() / 10.0f,
+ ui->audioMute->isChecked(),
+ ui->playLoop->isChecked());
+ }
+}
+
+void WFMModGUI::leaveEvent(QEvent*)
+{
+ blockApplySettings(true);
+ m_channelMarker.setHighlighted(false);
+ blockApplySettings(false);
+}
+
+void WFMModGUI::enterEvent(QEvent*)
+{
+ blockApplySettings(true);
+ m_channelMarker.setHighlighted(true);
+ blockApplySettings(false);
+}
+
+void WFMModGUI::tick()
+{
+ Real powDb = CalcDb::dbPower(m_wfmMod->getMagSq());
+ m_channelPowerDbAvg.feed(powDb);
+ ui->channelPower->setText(QString::number(m_channelPowerDbAvg.average(), 'f', 1));
+
+ if (((++m_tickCount & 0xf) == 0) && (m_modAFInput == WFMMod::WFMModInputFile))
+ {
+ WFMMod::MsgConfigureFileSourceStreamTiming* message = WFMMod::MsgConfigureFileSourceStreamTiming::create();
+ m_wfmMod->getInputMessageQueue()->push(message);
+ }
+}
+
+void WFMModGUI::updateWithStreamData()
+{
+ QTime recordLength(0, 0, 0, 0);
+ recordLength = recordLength.addSecs(m_recordLength);
+ QString s_time = recordLength.toString("hh:mm:ss");
+ ui->recordLengthText->setText(s_time);
+ updateWithStreamTime();
+}
+
+void WFMModGUI::updateWithStreamTime()
+{
+ int t_sec = 0;
+ int t_msec = 0;
+
+ if (m_recordSampleRate > 0)
+ {
+ t_msec = ((m_samplesCount * 1000) / m_recordSampleRate) % 1000;
+ t_sec = m_samplesCount / m_recordSampleRate;
+ }
+
+ QTime t(0, 0, 0, 0);
+ t = t.addSecs(t_sec);
+ t = t.addMSecs(t_msec);
+ QString s_timems = t.toString("hh:mm:ss.zzz");
+ QString s_time = t.toString("hh:mm:ss");
+ ui->relTimeText->setText(s_timems);
+
+ if (!m_enableNavTime)
+ {
+ float posRatio = (float) t_sec / (float) m_recordLength;
+ ui->navTimeSlider->setValue((int) (posRatio * 100.0));
+ }
+}
diff --git a/plugins/channeltx/modwfm/wfmmodgui.h b/plugins/channeltx/modwfm/wfmmodgui.h
new file mode 100644
index 000000000..f19d11047
--- /dev/null
+++ b/plugins/channeltx/modwfm/wfmmodgui.h
@@ -0,0 +1,121 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2016 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 //
+// //
+// 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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef PLUGINS_CHANNELTX_MODWFM_WFMMODGUI_H_
+#define PLUGINS_CHANNELTX_MODWFM_WFMMODGUI_H_
+
+#include "gui/rollupwidget.h"
+#include "plugin/plugingui.h"
+#include "dsp/channelmarker.h"
+#include "dsp/movingaverage.h"
+
+#include "wfmmod.h"
+
+class PluginAPI;
+class DeviceSinkAPI;
+
+class ThreadedBasebandSampleSource;
+class UpChannelizer;
+class WFMMod;
+
+namespace Ui {
+ class WFMModGUI;
+}
+
+class WFMModGUI : public RollupWidget, public PluginGUI {
+ Q_OBJECT
+
+public:
+ static WFMModGUI* create(PluginAPI* pluginAPI, DeviceSinkAPI *deviceAPI);
+ 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 bool handleMessage(const Message& message);
+
+ static const QString m_channelID;
+
+private slots:
+ void viewChanged();
+ void handleSourceMessages();
+
+ void on_deltaFrequency_changed(quint64 value);
+ void on_deltaMinus_toggled(bool minus);
+ void on_rfBW_currentIndexChanged(int index);
+ void on_afBW_valueChanged(int value);
+ void on_fmDev_valueChanged(int value);
+ void on_toneFrequency_valueChanged(int value);
+ void on_volume_valueChanged(int value);
+ void on_audioMute_toggled(bool checked);
+ void on_tone_toggled(bool checked);
+ void on_morseKeyer_toggled(bool checked);
+ void on_mic_toggled(bool checked);
+ void on_play_toggled(bool checked);
+
+ void on_playLoop_toggled(bool checked);
+ void on_navTimeSlider_valueChanged(int value);
+ void on_showFileDialog_clicked(bool checked);
+
+ void onWidgetRolled(QWidget* widget, bool rollDown);
+ void onMenuDoubleClicked();
+
+ void configureFileName();
+ void tick();
+
+private:
+ Ui::WFMModGUI* ui;
+ PluginAPI* m_pluginAPI;
+ DeviceSinkAPI* m_deviceAPI;
+ ChannelMarker m_channelMarker;
+ bool m_basicSettingsShown;
+ bool m_doApplySettings;
+
+ ThreadedBasebandSampleSource* m_threadedChannelizer;
+ UpChannelizer* m_channelizer;
+ WFMMod* m_wfmMod;
+ MovingAverage m_channelPowerDbAvg;
+
+ QString m_fileName;
+ quint32 m_recordLength;
+ int m_recordSampleRate;
+ int m_samplesCount;
+ std::size_t m_tickCount;
+ bool m_enableNavTime;
+ WFMMod::WFMModInputAF m_modAFInput;
+
+ static const int m_rfBW[];
+ static const int m_nbRfBW;
+
+ explicit WFMModGUI(PluginAPI* pluginAPI, DeviceSinkAPI *deviceAPI, QWidget* parent = NULL);
+ virtual ~WFMModGUI();
+
+ void blockApplySettings(bool block);
+ void applySettings();
+ void updateWithStreamData();
+ void updateWithStreamTime();
+
+ void leaveEvent(QEvent*);
+ void enterEvent(QEvent*);
+};
+
+#endif /* PLUGINS_CHANNELTX_MODWFM_WFMMODGUI_H_ */
diff --git a/plugins/channeltx/modwfm/wfmmodgui.ui b/plugins/channeltx/modwfm/wfmmodgui.ui
new file mode 100644
index 000000000..b96f8a3b1
--- /dev/null
+++ b/plugins/channeltx/modwfm/wfmmodgui.ui
@@ -0,0 +1,732 @@
+
+
+ WFMModGUI
+
+
+
+ 0
+ 0
+ 298
+ 300
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ Sans Serif
+ 9
+
+
+
+ Qt::StrongFocus
+
+
+ NFM Modulator
+
+
+
+
+ 2
+ 2
+ 280
+ 271
+
+
+
+
+ 280
+ 0
+
+
+
+ Settings
+
+
+
+ 3
+
+
+ 2
+
+ -
+
+
-
+
+
-
+
+
+ Frequency shift direction
+
+
+ ...
+
+
+
+ :/plus.png
+ :/minus.png
+
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 32
+ 16
+
+
+
+
+ Monospace
+ 12
+
+
+
+ SizeVerCursor
+
+
+ Qt::StrongFocus
+
+
+ Demod shift frequency from center in Hz
+
+
+
+ -
+
+
+ Hz
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Channel power
+
+
+ Qt::RightToLeft
+
+
+ 0.0
+
+
+
+ -
+
+
+ dB
+
+
+
+ -
+
+
+ Mute/Unmute audio
+
+
+ ...
+
+
+
+ :/sound_on.png
+ :/sound_off.png:/sound_on.png
+
+
+ true
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ RFBW
+
+
+
+ -
+
+
+
+ 70
+ 16777215
+
+
+
+
+ -
+
+
+
+ 10
+ 0
+
+
+
+ k
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ AFBW
+
+
+
+ -
+
+
+ Audio bandwidth
+
+
+ 1
+
+
+ 20
+
+
+ 1
+
+
+ 3
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 25
+ 0
+
+
+
+ 3k
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ -
+
+
-
+
+
+ Dev
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Modulation percentage
+
+
+ 100
+
+
+ 1
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 30
+ 0
+
+
+
+ 50k
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
-
+
+
+ Vol
+
+
+
+ -
+
+
+
+ 24
+ 24
+
+
+
+ Audio input gain
+
+
+ 100
+
+
+ 1
+
+
+ 10
+
+
+
+ -
+
+
+
+ 25
+ 0
+
+
+
+ Audio input gain value
+
+
+
+
+
+ 1.0
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Level (% full range) top trace: average, bottom trace: instantaneous peak, tip: peak hold
+
+
+
+
+
+ -
+
+
-
+
+
+ Tone modulation (1 kHz)
+
+
+ ...
+
+
+
+ :/carrier.png:/carrier.png
+
+
+
+ -
+
+
+ Morse keyer at tone frequency
+
+
+ ...
+
+
+
+ :/morsekey.png:/morsekey.png
+
+
+
+ -
+
+
+
+ 24
+ 24
+
+
+
+ Tone frequency
+
+
+ 10
+
+
+ 250
+
+
+ 1
+
+
+ 100
+
+
+
+ -
+
+
+
+ 36
+ 0
+
+
+
+ Tone frequency (kHz)
+
+
+ 1.00k
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Audio input
+
+
+ ...
+
+
+
+ :/microphone.png:/microphone.png
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+
-
+
+
+ ...
+
+
+
+
+
+ -
+
+
-
+
+
+
+ 24
+ 24
+
+
+
+
+ 24
+ 24
+
+
+
+ Open record file (48 kHz 32 bit float LE mono)
+
+
+
+
+
+
+ :/preset-load.png:/preset-load.png
+
+
+
+ -
+
+
+ Play record file in a loop
+
+
+ ...
+
+
+
+ :/playloop.png:/playloop.png
+
+
+
+ -
+
+
+ Record file play/pause
+
+
+ ...
+
+
+
+ :/play.png
+ :/pause.png
+ :/play.png
+ :/play.png:/play.png
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ false
+
+
+
+ 90
+ 0
+
+
+
+ Record time from start
+
+
+ 00:00:00.000
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ false
+
+
+
+ 60
+ 0
+
+
+
+ Total record time
+
+
+ 00:00:00
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+
+
+ -
+
+
-
+
+
+ Record file time navigator
+
+
+ 100
+
+
+ 1
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+
+
+
+
+ RollupWidget
+ QWidget
+
+ 1
+
+
+ ValueDial
+ QWidget
+
+ 1
+
+
+ ButtonSwitch
+ QToolButton
+
+
+
+ LevelMeterVU
+ QWidget
+
+ 1
+
+
+ CWKeyerGUI
+ QWidget
+
+ 1
+
+
+
+
+
+
+
diff --git a/plugins/channeltx/modwfm/wfmmodplugin.cpp b/plugins/channeltx/modwfm/wfmmodplugin.cpp
new file mode 100644
index 000000000..75c3b55d1
--- /dev/null
+++ b/plugins/channeltx/modwfm/wfmmodplugin.cpp
@@ -0,0 +1,65 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2016 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 //
+// //
+// 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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include "plugin/pluginapi.h"
+
+#include "wfmmodgui.h"
+#include "wfmmodplugin.h"
+
+const PluginDescriptor WFMModPlugin::m_pluginDescriptor = {
+ QString("WFM Modulator"),
+ QString("2.5.1"),
+ QString("(c) Edouard Griffiths, F4EXB"),
+ QString("https://github.com/f4exb/sdrangel"),
+ true,
+ QString("https://github.com/f4exb/sdrangel")
+};
+
+WFMModPlugin::WFMModPlugin(QObject* parent) :
+ QObject(parent)
+{
+}
+
+const PluginDescriptor& WFMModPlugin::getPluginDescriptor() const
+{
+ return m_pluginDescriptor;
+}
+
+void WFMModPlugin::initPlugin(PluginAPI* pluginAPI)
+{
+ m_pluginAPI = pluginAPI;
+
+ // register AM modulator
+ m_pluginAPI->registerTxChannel(WFMModGUI::m_channelID, this);
+}
+
+PluginGUI* WFMModPlugin::createTxChannel(const QString& channelName, DeviceSinkAPI *deviceAPI)
+{
+ if(channelName == WFMModGUI::m_channelID)
+ {
+ WFMModGUI* gui = WFMModGUI::create(m_pluginAPI, deviceAPI);
+ return gui;
+ } else {
+ return 0;
+ }
+}
+
+void WFMModPlugin::createInstanceModWFM(DeviceSinkAPI *deviceAPI)
+{
+ WFMModGUI* gui = WFMModGUI::create(m_pluginAPI, deviceAPI);
+}
diff --git a/plugins/channeltx/modwfm/wfmmodplugin.h b/plugins/channeltx/modwfm/wfmmodplugin.h
new file mode 100644
index 000000000..ed4465045
--- /dev/null
+++ b/plugins/channeltx/modwfm/wfmmodplugin.h
@@ -0,0 +1,47 @@
+///////////////////////////////////////////////////////////////////////////////////
+// Copyright (C) 2016 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 //
+// //
+// 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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDE_WFMMODPLUGIN_H
+#define INCLUDE_WFMMODPLUGIN_H
+
+#include
+#include "plugin/plugininterface.h"
+
+class DeviceSinkAPI;
+
+class WFMModPlugin : public QObject, PluginInterface {
+ Q_OBJECT
+ Q_INTERFACES(PluginInterface)
+ Q_PLUGIN_METADATA(IID "sdrangel.channeltx.wfmmod")
+
+public:
+ explicit WFMModPlugin(QObject* parent = NULL);
+
+ const PluginDescriptor& getPluginDescriptor() const;
+ void initPlugin(PluginAPI* pluginAPI);
+
+ PluginGUI* createTxChannel(const QString& channelName, DeviceSinkAPI *deviceAPI);
+
+private:
+ static const PluginDescriptor m_pluginDescriptor;
+
+ PluginAPI* m_pluginAPI;
+
+private slots:
+ void createInstanceModWFM(DeviceSinkAPI *deviceAPI);
+};
+
+#endif // INCLUDE_NFMMODPLUGIN_H